Migración completa a Clean Architecture con componentes funcionales

- Reorganización de estructura: Admin/, Public/, Shared/, Schemas/
- 12 componentes migrados: TopNotificationBar, Navbar, CtaLetsTalk, Hero,
  FeaturedImage, TableOfContents, CtaBoxSidebar, SocialShare, CtaPost,
  RelatedPost, ContactForm, Footer
- Panel de administración con tabs Bootstrap 5 funcionales
- Schemas JSON para configuración de componentes
- Renderers dinámicos con CSSGeneratorService (cero CSS hardcodeado)
- FormBuilders para UI admin con Design System consistente
- Fix: Bootstrap JS cargado en header para tabs funcionales
- Fix: buildTextInput maneja valores mixed (bool/string)
- Eliminación de estructura legacy (src/, admin/, assets/css/componente-*)

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
FrankZamora
2025-11-25 21:20:06 -06:00
parent 90de6df77c
commit 0846a3bf03
224 changed files with 21670 additions and 17816 deletions

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace ROITheme\Admin\Application\UseCases;
use ROITheme\Admin\Domain\Contracts\DashboardRendererInterface;
/**
* Caso de uso para renderizar el dashboard del panel de administración
*
* Application - Orquestación sin lógica de negocio ni WordPress
*/
final class RenderDashboardUseCase
{
/**
* @param DashboardRendererInterface $renderer Renderizador del dashboard
*/
public function __construct(
private readonly DashboardRendererInterface $renderer
) {
}
/**
* Ejecuta el caso de uso
*
* @param string $viewType Tipo de vista a renderizar
* @return string HTML renderizado
* @throws \RuntimeException Si el renderer no soporta el tipo de vista
*/
public function execute(string $viewType = 'dashboard'): string
{
if (!$this->renderer->supports($viewType)) {
throw new \RuntimeException(
sprintf('Renderer does not support view type: %s', $viewType)
);
}
return $this->renderer->render();
}
}

View File

@@ -0,0 +1,601 @@
<?php
declare(strict_types=1);
namespace ROITheme\Admin\ContactForm\Infrastructure\Ui;
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
/**
* FormBuilder para Contact Form
*
* RESPONSABILIDAD: Generar formulario de configuracion del Contact Form
*
* SEGURIDAD: El webhook_url se muestra como input type="password" para evitar
* que sea visible accidentalmente en pantalla compartida.
*
* @package ROITheme\Admin\ContactForm\Infrastructure\Ui
*/
final class ContactFormFormBuilder
{
public function __construct(
private AdminDashboardRenderer $renderer
) {}
public function buildForm(string $componentId): string
{
$html = '';
$html .= $this->buildHeader($componentId);
$html .= '<div class="row g-3">';
// Columna izquierda
$html .= '<div class="col-lg-6">';
$html .= $this->buildVisibilityGroup($componentId);
$html .= $this->buildContentGroup($componentId);
$html .= $this->buildContactInfoGroup($componentId);
$html .= $this->buildFormLabelsGroup($componentId);
$html .= '</div>';
// Columna derecha
$html .= '<div class="col-lg-6">';
$html .= $this->buildIntegrationGroup($componentId);
$html .= $this->buildMessagesGroup($componentId);
$html .= $this->buildColorsGroup($componentId);
$html .= $this->buildSpacingGroup($componentId);
$html .= $this->buildEffectsGroup($componentId);
$html .= '</div>';
$html .= '</div>';
return $html;
}
private function buildHeader(string $componentId): string
{
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
$html .= ' <div>';
$html .= ' <h3 class="h4 mb-1 fw-bold">';
$html .= ' <i class="bi bi-envelope-paper me-2" style="color: #FF8600;"></i>';
$html .= ' Configuracion de Formulario de Contacto';
$html .= ' </h3>';
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
$html .= ' Seccion de contacto antes del footer con envio a webhook';
$html .= ' </p>';
$html .= ' </div>';
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="contact_form">';
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
$html .= ' Restaurar valores por defecto';
$html .= ' </button>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildVisibilityGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
$html .= ' Visibilidad';
$html .= ' </h5>';
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
$html .= $this->buildSwitch('contactFormEnabled', 'Activar componente', 'bi-power', $enabled);
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
$html .= $this->buildSwitch('contactFormShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop);
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
$html .= $this->buildSwitch('contactFormShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all');
$html .= ' <div class="mb-0 mt-3">';
$html .= ' <label for="contactFormShowOnPages" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
$html .= ' Mostrar en';
$html .= ' </label>';
$html .= ' <select id="contactFormShowOnPages" class="form-select form-select-sm">';
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
$html .= ' </select>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildContentGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-card-text me-2" style="color: #FF8600;"></i>';
$html .= ' Contenido';
$html .= ' </h5>';
$sectionTitle = $this->renderer->getFieldValue($componentId, 'content', 'section_title', '¿Tienes alguna pregunta?');
$html .= ' <div class="mb-3">';
$html .= ' <label for="contactFormSectionTitle" class="form-label small mb-1 fw-semibold">Titulo de seccion</label>';
$html .= ' <input type="text" id="contactFormSectionTitle" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($sectionTitle) . '">';
$html .= ' </div>';
$sectionDescription = $this->renderer->getFieldValue($componentId, 'content', 'section_description', 'Completa el formulario y nuestro equipo te responderá en menos de 24 horas.');
$html .= ' <div class="mb-3">';
$html .= ' <label for="contactFormSectionDescription" class="form-label small mb-1 fw-semibold">Descripcion</label>';
$html .= ' <textarea id="contactFormSectionDescription" class="form-control form-control-sm" rows="2">';
$html .= esc_textarea($sectionDescription);
$html .= '</textarea>';
$html .= ' </div>';
$submitButtonText = $this->renderer->getFieldValue($componentId, 'content', 'submit_button_text', 'Enviar Mensaje');
$html .= ' <div class="mb-3">';
$html .= ' <label for="contactFormSubmitButtonText" class="form-label small mb-1 fw-semibold">Texto boton enviar</label>';
$html .= ' <input type="text" id="contactFormSubmitButtonText" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($submitButtonText) . '">';
$html .= ' </div>';
$submitButtonIcon = $this->renderer->getFieldValue($componentId, 'content', 'submit_button_icon', 'bi-send-fill');
$html .= ' <div class="mb-0">';
$html .= ' <label for="contactFormSubmitButtonIcon" class="form-label small mb-1 fw-semibold">Icono boton (Bootstrap Icons)</label>';
$html .= ' <input type="text" id="contactFormSubmitButtonIcon" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($submitButtonIcon) . '" placeholder="bi-send-fill">';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildContactInfoGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-person-lines-fill me-2" style="color: #FF8600;"></i>';
$html .= ' Info de Contacto';
$html .= ' </h5>';
$showContactInfo = $this->renderer->getFieldValue($componentId, 'contact_info', 'show_contact_info', true);
$html .= $this->buildSwitch('contactFormShowContactInfo', 'Mostrar info contacto', 'bi-eye', $showContactInfo);
$html .= ' <hr class="my-3">';
$html .= ' <p class="small fw-semibold mb-2">Telefono</p>';
$phoneLabel = $this->renderer->getFieldValue($componentId, 'contact_info', 'phone_label', 'Teléfono');
$html .= ' <div class="mb-2">';
$html .= ' <input type="text" id="contactFormPhoneLabel" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($phoneLabel) . '" placeholder="Label">';
$html .= ' </div>';
$phoneValue = $this->renderer->getFieldValue($componentId, 'contact_info', 'phone_value', '+52 55 1234 5678');
$html .= ' <div class="mb-3">';
$html .= ' <input type="text" id="contactFormPhoneValue" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($phoneValue) . '" placeholder="Numero">';
$html .= ' </div>';
$html .= ' <p class="small fw-semibold mb-2">Email</p>';
$emailLabel = $this->renderer->getFieldValue($componentId, 'contact_info', 'email_label', 'Email');
$html .= ' <div class="mb-2">';
$html .= ' <input type="text" id="contactFormEmailLabel" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($emailLabel) . '" placeholder="Label">';
$html .= ' </div>';
$emailValue = $this->renderer->getFieldValue($componentId, 'contact_info', 'email_value', 'contacto@apumexico.com');
$html .= ' <div class="mb-3">';
$html .= ' <input type="email" id="contactFormEmailValue" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($emailValue) . '" placeholder="Direccion">';
$html .= ' </div>';
$html .= ' <p class="small fw-semibold mb-2">Ubicacion</p>';
$locationLabel = $this->renderer->getFieldValue($componentId, 'contact_info', 'location_label', 'Ubicación');
$html .= ' <div class="mb-2">';
$html .= ' <input type="text" id="contactFormLocationLabel" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($locationLabel) . '" placeholder="Label">';
$html .= ' </div>';
$locationValue = $this->renderer->getFieldValue($componentId, 'contact_info', 'location_value', 'Ciudad de México, México');
$html .= ' <div class="mb-0">';
$html .= ' <input type="text" id="contactFormLocationValue" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($locationValue) . '" placeholder="Direccion">';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildFormLabelsGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-input-cursor-text me-2" style="color: #FF8600;"></i>';
$html .= ' Labels del Formulario';
$html .= ' </h5>';
$fullnamePlaceholder = $this->renderer->getFieldValue($componentId, 'form_labels', 'fullname_placeholder', 'Nombre completo *');
$html .= ' <div class="mb-2">';
$html .= ' <label for="contactFormFullnamePlaceholder" class="form-label small mb-1 fw-semibold">Placeholder nombre</label>';
$html .= ' <input type="text" id="contactFormFullnamePlaceholder" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($fullnamePlaceholder) . '">';
$html .= ' </div>';
$companyPlaceholder = $this->renderer->getFieldValue($componentId, 'form_labels', 'company_placeholder', 'Empresa');
$html .= ' <div class="mb-2">';
$html .= ' <label for="contactFormCompanyPlaceholder" class="form-label small mb-1 fw-semibold">Placeholder empresa</label>';
$html .= ' <input type="text" id="contactFormCompanyPlaceholder" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($companyPlaceholder) . '">';
$html .= ' </div>';
$whatsappPlaceholder = $this->renderer->getFieldValue($componentId, 'form_labels', 'whatsapp_placeholder', 'WhatsApp *');
$html .= ' <div class="mb-2">';
$html .= ' <label for="contactFormWhatsappPlaceholder" class="form-label small mb-1 fw-semibold">Placeholder WhatsApp</label>';
$html .= ' <input type="text" id="contactFormWhatsappPlaceholder" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($whatsappPlaceholder) . '">';
$html .= ' </div>';
$emailPlaceholder = $this->renderer->getFieldValue($componentId, 'form_labels', 'email_placeholder', 'Correo electrónico *');
$html .= ' <div class="mb-2">';
$html .= ' <label for="contactFormEmailPlaceholder" class="form-label small mb-1 fw-semibold">Placeholder email</label>';
$html .= ' <input type="text" id="contactFormEmailPlaceholder" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($emailPlaceholder) . '">';
$html .= ' </div>';
$messagePlaceholder = $this->renderer->getFieldValue($componentId, 'form_labels', 'message_placeholder', '¿En qué podemos ayudarte?');
$html .= ' <div class="mb-0">';
$html .= ' <label for="contactFormMessagePlaceholder" class="form-label small mb-1 fw-semibold">Placeholder mensaje</label>';
$html .= ' <input type="text" id="contactFormMessagePlaceholder" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($messagePlaceholder) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildIntegrationGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" 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-link-45deg me-2" style="color: #FF8600;"></i>';
$html .= ' Integracion Webhook';
$html .= ' <span class="badge bg-warning text-dark ms-2">Privado</span>';
$html .= ' </h5>';
$html .= ' <div class="alert alert-info py-2 small mb-3">';
$html .= ' <i class="bi bi-shield-lock me-1"></i>';
$html .= ' El webhook URL nunca se expone en el frontend. Los datos se envian de forma segura desde el servidor.';
$html .= ' </div>';
$webhookUrl = $this->renderer->getFieldValue($componentId, 'integration', 'webhook_url', '');
$html .= ' <div class="mb-3">';
$html .= ' <label for="contactFormWebhookUrl" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-link me-1" style="color: #FF8600;"></i>';
$html .= ' URL del Webhook';
$html .= ' </label>';
$html .= ' <textarea id="contactFormWebhookUrl" class="form-control form-control-sm" rows="2" ';
$html .= ' placeholder="https://tu-webhook.com/endpoint">';
$html .= esc_textarea($webhookUrl);
$html .= '</textarea>';
$html .= ' <small class="text-muted">Deja vacio si no deseas enviar a un webhook externo.</small>';
$html .= ' </div>';
$webhookMethod = $this->renderer->getFieldValue($componentId, 'integration', 'webhook_method', 'POST');
$html .= ' <div class="mb-3">';
$html .= ' <label for="contactFormWebhookMethod" class="form-label small mb-1 fw-semibold">Metodo HTTP</label>';
$html .= ' <select id="contactFormWebhookMethod" class="form-select form-select-sm">';
$html .= ' <option value="POST"' . ($webhookMethod === 'POST' ? ' selected' : '') . '>POST</option>';
$html .= ' <option value="GET"' . ($webhookMethod === 'GET' ? ' selected' : '') . '>GET</option>';
$html .= ' </select>';
$html .= ' </div>';
$includePageUrl = $this->renderer->getFieldValue($componentId, 'integration', 'include_page_url', true);
$html .= $this->buildSwitch('contactFormIncludePageUrl', 'Incluir URL de pagina', 'bi-link', $includePageUrl);
$includeTimestamp = $this->renderer->getFieldValue($componentId, 'integration', 'include_timestamp', true);
$html .= $this->buildSwitch('contactFormIncludeTimestamp', 'Incluir timestamp', 'bi-clock', $includeTimestamp);
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildMessagesGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-chat-quote me-2" style="color: #FF8600;"></i>';
$html .= ' Mensajes';
$html .= ' </h5>';
$successMessage = $this->renderer->getFieldValue($componentId, 'messages', 'success_message', '¡Gracias por contactarnos! Te responderemos pronto.');
$html .= ' <div class="mb-3">';
$html .= ' <label for="contactFormSuccessMessage" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-check-circle me-1 text-success"></i>';
$html .= ' Mensaje de exito';
$html .= ' </label>';
$html .= ' <textarea id="contactFormSuccessMessage" class="form-control form-control-sm" rows="2">';
$html .= esc_textarea($successMessage);
$html .= '</textarea>';
$html .= ' </div>';
$errorMessage = $this->renderer->getFieldValue($componentId, 'messages', 'error_message', 'Hubo un error al enviar el mensaje. Por favor intenta de nuevo.');
$html .= ' <div class="mb-3">';
$html .= ' <label for="contactFormErrorMessage" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-x-circle me-1 text-danger"></i>';
$html .= ' Mensaje de error';
$html .= ' </label>';
$html .= ' <textarea id="contactFormErrorMessage" class="form-control form-control-sm" rows="2">';
$html .= esc_textarea($errorMessage);
$html .= '</textarea>';
$html .= ' </div>';
$sendingMessage = $this->renderer->getFieldValue($componentId, 'messages', 'sending_message', 'Enviando...');
$html .= ' <div class="mb-3">';
$html .= ' <label for="contactFormSendingMessage" class="form-label small mb-1 fw-semibold">Mensaje enviando</label>';
$html .= ' <input type="text" id="contactFormSendingMessage" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($sendingMessage) . '">';
$html .= ' </div>';
$validationRequired = $this->renderer->getFieldValue($componentId, 'messages', 'validation_required', 'Este campo es obligatorio');
$html .= ' <div class="mb-2">';
$html .= ' <label for="contactFormValidationRequired" class="form-label small mb-1 fw-semibold">Error campo requerido</label>';
$html .= ' <input type="text" id="contactFormValidationRequired" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($validationRequired) . '">';
$html .= ' </div>';
$validationEmail = $this->renderer->getFieldValue($componentId, 'messages', 'validation_email', 'Por favor ingresa un email válido');
$html .= ' <div class="mb-0">';
$html .= ' <label for="contactFormValidationEmail" class="form-label small mb-1 fw-semibold">Error email invalido</label>';
$html .= ' <input type="text" id="contactFormValidationEmail" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($validationEmail) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildColorsGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
$html .= ' Colores';
$html .= ' </h5>';
// Seccion
$html .= ' <p class="small fw-semibold mb-2">Seccion</p>';
$html .= ' <div class="row g-2 mb-3">';
$sectionBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'section_bg_color', 'rgba(108, 117, 125, 0.25)');
$html .= $this->buildColorPicker('contactFormSectionBgColor', 'Fondo seccion', $sectionBgColor);
$titleColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_color', '#212529');
$html .= $this->buildColorPicker('contactFormTitleColor', 'Color titulo', $titleColor);
$html .= ' </div>';
// Boton
$html .= ' <p class="small fw-semibold mb-2">Boton</p>';
$html .= ' <div class="row g-2 mb-3">';
$buttonBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'button_bg_color', '#FF8600');
$html .= $this->buildColorPicker('contactFormButtonBgColor', 'Fondo boton', $buttonBgColor);
$buttonTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'button_text_color', '#ffffff');
$html .= $this->buildColorPicker('contactFormButtonTextColor', 'Texto boton', $buttonTextColor);
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-3">';
$buttonHoverBg = $this->renderer->getFieldValue($componentId, 'colors', 'button_hover_bg', '#e67a00');
$html .= $this->buildColorPicker('contactFormButtonHoverBg', 'Hover boton', $buttonHoverBg);
$iconColor = $this->renderer->getFieldValue($componentId, 'colors', 'icon_color', '#FF8600');
$html .= $this->buildColorPicker('contactFormIconColor', 'Iconos', $iconColor);
$html .= ' </div>';
// Mensajes
$html .= ' <p class="small fw-semibold mb-2">Mensajes</p>';
$html .= ' <div class="row g-2 mb-0">';
$successBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'success_bg_color', '#d1e7dd');
$html .= $this->buildColorPicker('contactFormSuccessBgColor', 'Fondo exito', $successBgColor);
$errorBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'error_bg_color', '#f8d7da');
$html .= $this->buildColorPicker('contactFormErrorBgColor', 'Fondo error', $errorBgColor);
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildSpacingGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-arrows-move me-2" style="color: #FF8600;"></i>';
$html .= ' Espaciado';
$html .= ' </h5>';
$html .= ' <div class="row g-2 mb-3">';
$sectionPaddingY = $this->renderer->getFieldValue($componentId, 'spacing', 'section_padding_y', '3rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="contactFormSectionPaddingY" class="form-label small mb-1 fw-semibold">Padding vertical</label>';
$html .= ' <input type="text" id="contactFormSectionPaddingY" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($sectionPaddingY) . '">';
$html .= ' </div>';
$sectionMarginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'section_margin_top', '3rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="contactFormSectionMarginTop" class="form-label small mb-1 fw-semibold">Margen superior</label>';
$html .= ' <input type="text" id="contactFormSectionMarginTop" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($sectionMarginTop) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-0">';
$titleMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'title_margin_bottom', '0.75rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="contactFormTitleMarginBottom" class="form-label small mb-1 fw-semibold">Margen titulo</label>';
$html .= ' <input type="text" id="contactFormTitleMarginBottom" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($titleMarginBottom) . '">';
$html .= ' </div>';
$formGap = $this->renderer->getFieldValue($componentId, 'spacing', 'form_gap', '1rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="contactFormFormGap" class="form-label small mb-1 fw-semibold">Espacio campos</label>';
$html .= ' <input type="text" id="contactFormFormGap" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($formGap) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildEffectsGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-magic me-2" style="color: #FF8600;"></i>';
$html .= ' Efectos Visuales';
$html .= ' </h5>';
$html .= ' <div class="row g-2 mb-3">';
$inputBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'input_border_radius', '6px');
$html .= ' <div class="col-6">';
$html .= ' <label for="contactFormInputBorderRadius" class="form-label small mb-1 fw-semibold">Radio inputs</label>';
$html .= ' <input type="text" id="contactFormInputBorderRadius" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($inputBorderRadius) . '">';
$html .= ' </div>';
$buttonBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_border_radius', '6px');
$html .= ' <div class="col-6">';
$html .= ' <label for="contactFormButtonBorderRadius" class="form-label small mb-1 fw-semibold">Radio boton</label>';
$html .= ' <input type="text" id="contactFormButtonBorderRadius" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($buttonBorderRadius) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-3">';
$buttonPadding = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_padding', '0.75rem 2rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="contactFormButtonPadding" class="form-label small mb-1 fw-semibold">Padding boton</label>';
$html .= ' <input type="text" id="contactFormButtonPadding" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($buttonPadding) . '">';
$html .= ' </div>';
$transitionDuration = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s');
$html .= ' <div class="col-6">';
$html .= ' <label for="contactFormTransitionDuration" class="form-label small mb-1 fw-semibold">Duracion transicion</label>';
$html .= ' <input type="text" id="contactFormTransitionDuration" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($transitionDuration) . '">';
$html .= ' </div>';
$html .= ' </div>';
$textareaRows = $this->renderer->getFieldValue($componentId, 'visual_effects', 'textarea_rows', '4');
$html .= ' <div class="mb-0">';
$html .= ' <label for="contactFormTextareaRows" class="form-label small mb-1 fw-semibold">Filas textarea</label>';
$html .= ' <input type="number" id="contactFormTextareaRows" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($textareaRows) . '" min="2" max="10">';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildSwitch(string $id, string $label, string $icon, mixed $checked): string
{
$checked = $checked === true || $checked === '1' || $checked === 1;
$html = ' <div class="mb-2">';
$html .= ' <div class="form-check form-switch">';
$html .= sprintf(
' <input class="form-check-input" type="checkbox" id="%s" %s>',
esc_attr($id),
$checked ? 'checked' : ''
);
$html .= sprintf(
' <label class="form-check-label small" for="%s">',
esc_attr($id)
);
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
$html .= sprintf(' <strong>%s</strong>', esc_html($label));
$html .= ' </label>';
$html .= ' </div>';
$html .= ' </div>';
return $html;
}
private function buildColorPicker(string $id, string $label, string $value): string
{
// Manejar colores rgba
$colorValue = $value;
if (strpos($value, 'rgba') === 0 || strpos($value, 'rgb') === 0) {
// Para rgba usamos un color aproximado en el picker
$colorValue = '#6c757d';
}
$html = ' <div class="col-6">';
$html .= sprintf(
' <label class="form-label small fw-semibold">%s</label>',
esc_html($label)
);
$html .= ' <div class="input-group input-group-sm">';
$html .= sprintf(
' <input type="color" class="form-control form-control-color" id="%s" value="%s">',
esc_attr($id),
esc_attr($colorValue)
);
$html .= sprintf(
' <input type="text" class="form-control" id="%sText" value="%s" style="font-size: 0.75rem;">',
esc_attr($id),
esc_attr($value)
);
$html .= ' </div>';
$html .= ' </div>';
return $html;
}
}

View File

@@ -0,0 +1,518 @@
<?php
declare(strict_types=1);
namespace ROITheme\Admin\CtaBoxSidebar\Infrastructure\Ui;
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
/**
* FormBuilder para el CTA Box Sidebar
*
* Responsabilidad:
* - Generar HTML del formulario de configuracion
* - Usar Design System (Bootstrap 5)
* - Cargar valores desde BD via AdminDashboardRenderer
*
* @package ROITheme\Admin\CtaBoxSidebar\Infrastructure\Ui
*/
final class CtaBoxSidebarFormBuilder
{
public function __construct(
private AdminDashboardRenderer $renderer
) {}
public function buildForm(string $componentId): string
{
$html = '';
$html .= $this->buildHeader($componentId);
$html .= '<div class="row g-3">';
// Columna izquierda
$html .= '<div class="col-lg-6">';
$html .= $this->buildVisibilityGroup($componentId);
$html .= $this->buildContentGroup($componentId);
$html .= $this->buildBehaviorGroup($componentId);
$html .= '</div>';
// Columna derecha
$html .= '<div class="col-lg-6">';
$html .= $this->buildTypographyGroup($componentId);
$html .= $this->buildColorsGroup($componentId);
$html .= $this->buildSpacingGroup($componentId);
$html .= $this->buildEffectsGroup($componentId);
$html .= '</div>';
$html .= '</div>';
return $html;
}
private function buildHeader(string $componentId): string
{
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
$html .= ' <div>';
$html .= ' <h3 class="h4 mb-1 fw-bold">';
$html .= ' <i class="bi bi-megaphone me-2" style="color: #FF8600;"></i>';
$html .= ' Configuracion de CTA Box Sidebar';
$html .= ' </h3>';
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
$html .= ' Caja de llamada a la accion en el sidebar';
$html .= ' </p>';
$html .= ' </div>';
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="cta_box_sidebar">';
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
$html .= ' Restaurar valores por defecto';
$html .= ' </button>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildVisibilityGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
$html .= ' Visibilidad';
$html .= ' </h5>';
// is_enabled
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
$html .= $this->buildSwitch('ctaEnabled', 'Activar CTA box', 'bi-power', $enabled);
// show_on_desktop
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
$html .= $this->buildSwitch('ctaShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop);
// show_on_mobile
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', false);
$html .= $this->buildSwitch('ctaShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
// show_on_pages
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
$html .= ' <div class="mb-0 mt-3">';
$html .= ' <label for="ctaShowOnPages" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
$html .= ' Mostrar en';
$html .= ' </label>';
$html .= ' <select id="ctaShowOnPages" class="form-select form-select-sm">';
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
$html .= ' </select>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildContentGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-card-text me-2" style="color: #FF8600;"></i>';
$html .= ' Contenido';
$html .= ' </h5>';
// title
$title = $this->renderer->getFieldValue($componentId, 'content', 'title', '¿Listo para potenciar tus proyectos?');
$html .= ' <div class="mb-3">';
$html .= ' <label for="ctaTitle" class="form-label small mb-1 fw-semibold">Titulo</label>';
$html .= ' <input type="text" id="ctaTitle" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($title) . '">';
$html .= ' </div>';
// description
$description = $this->renderer->getFieldValue($componentId, 'content', 'description', 'Accede a nuestra biblioteca completa de APUs y herramientas profesionales.');
$html .= ' <div class="mb-3">';
$html .= ' <label for="ctaDescription" class="form-label small mb-1 fw-semibold">Descripcion</label>';
$html .= ' <textarea id="ctaDescription" class="form-control form-control-sm" rows="2">' . esc_textarea($description) . '</textarea>';
$html .= ' </div>';
// button_text
$buttonText = $this->renderer->getFieldValue($componentId, 'content', 'button_text', 'Solicitar Demo');
$html .= ' <div class="mb-3">';
$html .= ' <label for="ctaButtonText" class="form-label small mb-1 fw-semibold">Texto del boton</label>';
$html .= ' <input type="text" id="ctaButtonText" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($buttonText) . '">';
$html .= ' </div>';
// button_icon
$buttonIcon = $this->renderer->getFieldValue($componentId, 'content', 'button_icon', 'bi bi-calendar-check');
$html .= ' <div class="mb-3">';
$html .= ' <label for="ctaButtonIcon" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-stars me-1" style="color: #FF8600;"></i>';
$html .= ' Icono del boton';
$html .= ' </label>';
$html .= ' <input type="text" id="ctaButtonIcon" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($buttonIcon) . '" placeholder="ej: bi bi-calendar-check">';
$html .= ' <small class="text-muted">Clase de Bootstrap Icons</small>';
$html .= ' </div>';
// button_action
$buttonAction = $this->renderer->getFieldValue($componentId, 'content', 'button_action', 'modal');
$html .= ' <div class="mb-3">';
$html .= ' <label for="ctaButtonAction" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-cursor me-1" style="color: #FF8600;"></i>';
$html .= ' Accion del boton';
$html .= ' </label>';
$html .= ' <select id="ctaButtonAction" class="form-select form-select-sm">';
$html .= ' <option value="modal"' . ($buttonAction === 'modal' ? ' selected' : '') . '>Abrir modal</option>';
$html .= ' <option value="link"' . ($buttonAction === 'link' ? ' selected' : '') . '>Ir a URL</option>';
$html .= ' <option value="scroll"' . ($buttonAction === 'scroll' ? ' selected' : '') . '>Scroll a seccion</option>';
$html .= ' </select>';
$html .= ' </div>';
// button_link
$buttonLink = $this->renderer->getFieldValue($componentId, 'content', 'button_link', '#contactModal');
$html .= ' <div class="mb-0">';
$html .= ' <label for="ctaButtonLink" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-link-45deg me-1" style="color: #FF8600;"></i>';
$html .= ' URL/ID destino';
$html .= ' </label>';
$html .= ' <input type="text" id="ctaButtonLink" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($buttonLink) . '" placeholder="ej: #contactModal o https://...">';
$html .= ' <small class="text-muted">Para modal usa #nombreModal, para scroll usa #idSeccion</small>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildBehaviorGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-sliders me-2" style="color: #FF8600;"></i>';
$html .= ' Comportamiento';
$html .= ' </h5>';
// text_align
$textAlign = $this->renderer->getFieldValue($componentId, 'behavior', 'text_align', 'center');
$html .= ' <div class="mb-0">';
$html .= ' <label for="ctaTextAlign" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-text-center me-1" style="color: #FF8600;"></i>';
$html .= ' Alineacion del texto';
$html .= ' </label>';
$html .= ' <select id="ctaTextAlign" class="form-select form-select-sm">';
$html .= ' <option value="left"' . ($textAlign === 'left' ? ' selected' : '') . '>Izquierda</option>';
$html .= ' <option value="center"' . ($textAlign === 'center' ? ' selected' : '') . '>Centro</option>';
$html .= ' <option value="right"' . ($textAlign === 'right' ? ' selected' : '') . '>Derecha</option>';
$html .= ' </select>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildTypographyGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-fonts me-2" style="color: #FF8600;"></i>';
$html .= ' Tipografia';
$html .= ' </h5>';
$html .= ' <div class="row g-2 mb-3">';
// title_font_size
$titleFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_size', '1.25rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="ctaTitleFontSize" class="form-label small mb-1 fw-semibold">Tamano titulo</label>';
$html .= ' <input type="text" id="ctaTitleFontSize" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($titleFontSize) . '">';
$html .= ' </div>';
// title_font_weight
$titleFontWeight = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_weight', '700');
$html .= ' <div class="col-6">';
$html .= ' <label for="ctaTitleFontWeight" class="form-label small mb-1 fw-semibold">Peso titulo</label>';
$html .= ' <input type="text" id="ctaTitleFontWeight" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($titleFontWeight) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-3">';
// description_font_size
$descFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'description_font_size', '0.9rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="ctaDescFontSize" class="form-label small mb-1 fw-semibold">Tamano descripcion</label>';
$html .= ' <input type="text" id="ctaDescFontSize" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($descFontSize) . '">';
$html .= ' </div>';
// button_font_size
$buttonFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'button_font_size', '1rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="ctaButtonFontSize" class="form-label small mb-1 fw-semibold">Tamano boton</label>';
$html .= ' <input type="text" id="ctaButtonFontSize" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($buttonFontSize) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-0">';
// button_font_weight
$buttonFontWeight = $this->renderer->getFieldValue($componentId, 'typography', 'button_font_weight', '700');
$html .= ' <div class="col-6">';
$html .= ' <label for="ctaButtonFontWeight" class="form-label small mb-1 fw-semibold">Peso boton</label>';
$html .= ' <input type="text" id="ctaButtonFontWeight" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($buttonFontWeight) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildColorsGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
$html .= ' Colores';
$html .= ' </h5>';
// Colores principales
$html .= ' <p class="small fw-semibold mb-2">Contenedor</p>';
$html .= ' <div class="row g-2 mb-3">';
$bgColor = $this->renderer->getFieldValue($componentId, 'colors', 'background_color', '#FF8600');
$html .= $this->buildColorPicker('ctaBackgroundColor', 'Fondo', $bgColor);
$titleColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_color', '#ffffff');
$html .= $this->buildColorPicker('ctaTitleColor', 'Titulo', $titleColor);
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-3">';
$descColor = $this->renderer->getFieldValue($componentId, 'colors', 'description_color', 'rgba(255, 255, 255, 0.95)');
$html .= $this->buildColorPicker('ctaDescriptionColor', 'Descripcion', $descColor);
$html .= ' </div>';
// Colores del boton
$html .= ' <p class="small fw-semibold mb-2">Boton</p>';
$html .= ' <div class="row g-2 mb-3">';
$buttonBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'button_background_color', '#ffffff');
$html .= $this->buildColorPicker('ctaButtonBgColor', 'Fondo', $buttonBgColor);
$buttonTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'button_text_color', '#FF8600');
$html .= $this->buildColorPicker('ctaButtonTextColor', 'Texto', $buttonTextColor);
$html .= ' </div>';
// Colores hover
$html .= ' <p class="small fw-semibold mb-2">Boton Hover</p>';
$html .= ' <div class="row g-2 mb-0">';
$buttonHoverBg = $this->renderer->getFieldValue($componentId, 'colors', 'button_hover_background', '#0E2337');
$html .= $this->buildColorPicker('ctaButtonHoverBg', 'Fondo hover', $buttonHoverBg);
$buttonHoverText = $this->renderer->getFieldValue($componentId, 'colors', 'button_hover_text_color', '#ffffff');
$html .= $this->buildColorPicker('ctaButtonHoverText', 'Texto hover', $buttonHoverText);
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildSpacingGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-arrows-move me-2" style="color: #FF8600;"></i>';
$html .= ' Espaciado';
$html .= ' </h5>';
$html .= ' <div class="row g-2 mb-3">';
// container_padding
$containerPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'container_padding', '24px');
$html .= ' <div class="col-6">';
$html .= ' <label for="ctaContainerPadding" class="form-label small mb-1 fw-semibold">Padding contenedor</label>';
$html .= ' <input type="text" id="ctaContainerPadding" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($containerPadding) . '">';
$html .= ' </div>';
// title_margin_bottom
$titleMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'title_margin_bottom', '1rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="ctaTitleMarginBottom" class="form-label small mb-1 fw-semibold">Margen titulo</label>';
$html .= ' <input type="text" id="ctaTitleMarginBottom" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($titleMarginBottom) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-3">';
// description_margin_bottom
$descMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'description_margin_bottom', '1rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="ctaDescMarginBottom" class="form-label small mb-1 fw-semibold">Margen descripcion</label>';
$html .= ' <input type="text" id="ctaDescMarginBottom" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($descMarginBottom) . '">';
$html .= ' </div>';
// button_padding
$buttonPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'button_padding', '0.75rem 1.5rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="ctaButtonPadding" class="form-label small mb-1 fw-semibold">Padding boton</label>';
$html .= ' <input type="text" id="ctaButtonPadding" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($buttonPadding) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-0">';
// icon_margin_right
$iconMarginRight = $this->renderer->getFieldValue($componentId, 'spacing', 'icon_margin_right', '0.5rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="ctaIconMarginRight" class="form-label small mb-1 fw-semibold">Margen icono</label>';
$html .= ' <input type="text" id="ctaIconMarginRight" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($iconMarginRight) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildEffectsGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-magic me-2" style="color: #FF8600;"></i>';
$html .= ' Efectos Visuales';
$html .= ' </h5>';
$html .= ' <div class="row g-2 mb-3">';
// border_radius
$borderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_radius', '8px');
$html .= ' <div class="col-6">';
$html .= ' <label for="ctaBorderRadius" class="form-label small mb-1 fw-semibold">Radio contenedor</label>';
$html .= ' <input type="text" id="ctaBorderRadius" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($borderRadius) . '">';
$html .= ' </div>';
// button_border_radius
$buttonBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_border_radius', '8px');
$html .= ' <div class="col-6">';
$html .= ' <label for="ctaButtonBorderRadius" class="form-label small mb-1 fw-semibold">Radio boton</label>';
$html .= ' <input type="text" id="ctaButtonBorderRadius" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($buttonBorderRadius) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-0">';
// box_shadow
$boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', '0 4px 12px rgba(255, 133, 0, 0.2)');
$html .= ' <div class="col-12">';
$html .= ' <label for="ctaBoxShadow" class="form-label small mb-1 fw-semibold">Sombra</label>';
$html .= ' <input type="text" id="ctaBoxShadow" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($boxShadow) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' <div class="row g-2 mt-3 mb-0">';
// transition_duration
$transitionDuration = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s');
$html .= ' <div class="col-6">';
$html .= ' <label for="ctaTransitionDuration" class="form-label small mb-1 fw-semibold">Duracion transicion</label>';
$html .= ' <input type="text" id="ctaTransitionDuration" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($transitionDuration) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildSwitch(string $id, string $label, string $icon, bool $checked): string
{
$html = ' <div class="mb-2">';
$html .= ' <div class="form-check form-switch">';
$html .= sprintf(
' <input class="form-check-input" type="checkbox" id="%s" %s>',
esc_attr($id),
$checked ? 'checked' : ''
);
$html .= sprintf(
' <label class="form-check-label small" for="%s">',
esc_attr($id)
);
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
$html .= sprintf(' <strong>%s</strong>', esc_html($label));
$html .= ' </label>';
$html .= ' </div>';
$html .= ' </div>';
return $html;
}
private function buildColorPicker(string $id, string $label, string $value): string
{
$html = ' <div class="col-6">';
$html .= sprintf(
' <label class="form-label small fw-semibold">%s</label>',
esc_html($label)
);
$html .= ' <div class="input-group input-group-sm">';
$html .= sprintf(
' <input type="color" class="form-control form-control-color" id="%s" value="%s">',
esc_attr($id),
esc_attr($value)
);
$html .= sprintf(
' <span class="input-group-text" id="%sValue">%s</span>',
esc_attr($id),
esc_html(strtoupper($value))
);
$html .= ' </div>';
$html .= ' </div>';
return $html;
}
}

View File

@@ -0,0 +1,450 @@
<?php
declare(strict_types=1);
namespace ROITheme\Admin\CtaLetsTalk\Infrastructure\Ui;
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
/**
* Class CtaLetsTalkFormBuilder
*
* Genera el formulario de administración para el componente CTA "Let's Talk".
*
* Responsabilidades:
* - Renderizar formulario de configuración del botón CTA
* - Organizar campos en grupos según el schema JSON
* - Aplicar Design System (gradiente navy, borde orange)
* - Usar Bootstrap 5 form controls
*
* @package ROITheme\Admin\CtaLetsTalk\Infrastructure\Ui
*/
final class CtaLetsTalkFormBuilder
{
private const COMPONENT_ID = 'cta-lets-talk';
public function __construct(
private AdminDashboardRenderer $renderer
) {}
public function buildForm(string $componentId): string
{
$html = '';
// Header
$html .= $this->buildHeader($componentId);
// Layout 2 columnas
$html .= '<div class="row g-3">';
$html .= ' <div class="col-lg-6">';
$html .= $this->buildVisibilityGroup($componentId);
$html .= $this->buildContentGroup($componentId);
$html .= $this->buildBehaviorGroup($componentId);
$html .= ' </div>';
$html .= ' <div class="col-lg-6">';
$html .= $this->buildTypographyGroup($componentId);
$html .= $this->buildColorsGroup($componentId);
$html .= $this->buildSpacingGroup($componentId);
$html .= $this->buildVisualEffectsGroup($componentId);
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildHeader(string $componentId): string
{
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
$html .= ' <div>';
$html .= ' <h3 class="h4 mb-1 fw-bold">';
$html .= ' <i class="bi bi-lightning-charge-fill me-2" style="color: #FF8600;"></i>';
$html .= ' Configuración del Botón "Let\'s Talk"';
$html .= ' </h3>';
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
$html .= ' Personaliza el botón CTA principal del navbar';
$html .= ' </p>';
$html .= ' </div>';
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="cta-lets-talk">';
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
$html .= ' Restaurar valores por defecto';
$html .= ' </button>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildVisibilityGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
$html .= ' Visibilidad';
$html .= ' </h5>';
// Switch: Enabled
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
$html .= ' <div class="mb-2">';
$html .= ' <div class="form-check form-switch">';
$html .= ' <input class="form-check-input" type="checkbox" id="ctaLetsTalkEnabled" name="visibility[is_enabled]" ';
$html .= checked($enabled, true, false) . '>';
$html .= ' <label class="form-check-label small" for="ctaLetsTalkEnabled">';
$html .= ' <strong>Mostrar botón Let\'s Talk</strong>';
$html .= ' </label>';
$html .= ' </div>';
$html .= ' </div>';
// Switch: Show on Desktop
$showDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
$html .= ' <div class="mb-2">';
$html .= ' <div class="form-check form-switch">';
$html .= ' <input class="form-check-input" type="checkbox" id="ctaLetsTalkShowDesktop" name="visibility[show_on_desktop]" ';
$html .= checked($showDesktop, true, false) . '>';
$html .= ' <label class="form-check-label small" for="ctaLetsTalkShowDesktop">';
$html .= ' <strong>Mostrar en escritorio</strong> <span class="text-muted">(≥992px)</span>';
$html .= ' </label>';
$html .= ' </div>';
$html .= ' </div>';
// Switch: Show on Mobile
$showMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', false);
$html .= ' <div class="mb-2">';
$html .= ' <div class="form-check form-switch">';
$html .= ' <input class="form-check-input" type="checkbox" id="ctaLetsTalkShowMobile" name="visibility[show_on_mobile]" ';
$html .= checked($showMobile, true, false) . '>';
$html .= ' <label class="form-check-label small" for="ctaLetsTalkShowMobile">';
$html .= ' <strong>Mostrar en móvil</strong> <span class="text-muted">(<992px)</span>';
$html .= ' </label>';
$html .= ' </div>';
$html .= ' </div>';
// Select: Show on Pages
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all');
$html .= ' <div class="mb-0">';
$html .= ' <label for="ctaLetsTalkShowOnPages" class="form-label small mb-1 fw-semibold">Mostrar en</label>';
$html .= ' <select id="ctaLetsTalkShowOnPages" name="visibility[show_on_pages]" class="form-select form-select-sm">';
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las páginas</option>';
$html .= ' <option value="home" ' . selected($showOnPages, 'home', false) . '>Solo página de inicio</option>';
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo páginas</option>';
$html .= ' </select>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildContentGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-type me-2" style="color: #FF8600;"></i>';
$html .= ' Contenido';
$html .= ' </h5>';
// Text: Button Text
$buttonText = $this->renderer->getFieldValue($componentId, 'content', 'button_text', "Let's Talk");
$html .= ' <div class="mb-2">';
$html .= ' <label for="ctaLetsTalkButtonText" class="form-label small mb-1 fw-semibold">Texto del botón</label>';
$html .= ' <input type="text" id="ctaLetsTalkButtonText" name="content[button_text]" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($buttonText) . '" maxlength="30" placeholder="Let\'s Talk">';
$html .= ' </div>';
// Switch: Show Icon
$showIcon = $this->renderer->getFieldValue($componentId, 'content', 'show_icon', true);
$html .= ' <div class="mb-2">';
$html .= ' <div class="form-check form-switch">';
$html .= ' <input class="form-check-input" type="checkbox" id="ctaLetsTalkShowIcon" name="content[show_icon]" ';
$html .= checked($showIcon, true, false) . '>';
$html .= ' <label class="form-check-label small" for="ctaLetsTalkShowIcon">';
$html .= ' <strong>Mostrar ícono</strong>';
$html .= ' </label>';
$html .= ' </div>';
$html .= ' </div>';
// Text: Icon Class
$iconClass = $this->renderer->getFieldValue($componentId, 'content', 'icon_class', 'bi-lightning-charge-fill');
$html .= ' <div class="mb-2">';
$html .= ' <label for="ctaLetsTalkIconClass" class="form-label small mb-1 fw-semibold">';
$html .= ' Clase del ícono <a href="https://icons.getbootstrap.com/" target="_blank" class="text-decoration-none"><i class="bi bi-box-arrow-up-right"></i></a>';
$html .= ' </label>';
$html .= ' <input type="text" id="ctaLetsTalkIconClass" name="content[icon_class]" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($iconClass) . '" placeholder="bi-lightning-charge-fill">';
$html .= ' <small class="text-muted">Usa clases de Bootstrap Icons (ej: bi-chat-dots)</small>';
$html .= ' </div>';
// Text: Modal Target
$modalTarget = $this->renderer->getFieldValue($componentId, 'content', 'modal_target', '#contactModal');
$html .= ' <div class="mb-2">';
$html .= ' <label for="ctaLetsTalkModalTarget" class="form-label small mb-1 fw-semibold">ID del modal</label>';
$html .= ' <input type="text" id="ctaLetsTalkModalTarget" name="content[modal_target]" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($modalTarget) . '" placeholder="#contactModal">';
$html .= ' </div>';
// Text: ARIA Label
$ariaLabel = $this->renderer->getFieldValue($componentId, 'content', 'aria_label', 'Abrir formulario de contacto');
$html .= ' <div class="mb-0">';
$html .= ' <label for="ctaLetsTalkAriaLabel" class="form-label small mb-1 fw-semibold">Etiqueta ARIA (accesibilidad)</label>';
$html .= ' <input type="text" id="ctaLetsTalkAriaLabel" name="content[aria_label]" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($ariaLabel) . '" maxlength="100">';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildBehaviorGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-mouse me-2" style="color: #FF8600;"></i>';
$html .= ' Comportamiento';
$html .= ' </h5>';
// Switch: Enable Modal
$enableModal = $this->renderer->getFieldValue($componentId, 'behavior', 'enable_modal', true);
$html .= ' <div class="mb-2">';
$html .= ' <div class="form-check form-switch">';
$html .= ' <input class="form-check-input" type="checkbox" id="ctaLetsTalkEnableModal" name="behavior[enable_modal]" ';
$html .= checked($enableModal, true, false) . '>';
$html .= ' <label class="form-check-label small" for="ctaLetsTalkEnableModal">';
$html .= ' <strong>Abrir modal al hacer clic</strong>';
$html .= ' </label>';
$html .= ' </div>';
$html .= ' <small class="text-muted">Si está desactivado, usará la URL personalizada</small>';
$html .= ' </div>';
// URL: Custom URL
$customUrl = $this->renderer->getFieldValue($componentId, 'behavior', 'custom_url', '');
$html .= ' <div class="mb-2">';
$html .= ' <label for="ctaLetsTalkCustomUrl" class="form-label small mb-1 fw-semibold">URL personalizada</label>';
$html .= ' <input type="url" id="ctaLetsTalkCustomUrl" name="behavior[custom_url]" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($customUrl) . '" placeholder="https://ejemplo.com/contacto">';
$html .= ' <small class="text-muted">Solo se usa si "Abrir modal" está desactivado</small>';
$html .= ' </div>';
// Switch: Open in New Tab
$openNewTab = $this->renderer->getFieldValue($componentId, 'behavior', 'open_in_new_tab', false);
$html .= ' <div class="mb-0">';
$html .= ' <div class="form-check form-switch">';
$html .= ' <input class="form-check-input" type="checkbox" id="ctaLetsTalkOpenNewTab" name="behavior[open_in_new_tab]" ';
$html .= checked($openNewTab, true, false) . '>';
$html .= ' <label class="form-check-label small" for="ctaLetsTalkOpenNewTab">';
$html .= ' <strong>Abrir en nueva pestaña</strong>';
$html .= ' </label>';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildTypographyGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-fonts me-2" style="color: #FF8600;"></i>';
$html .= ' Tipografía';
$html .= ' </h5>';
// Text: Font Size
$fontSize = $this->renderer->getFieldValue($componentId, 'typography', 'font_size', '1rem');
$html .= ' <div class="mb-2">';
$html .= ' <label for="ctaLetsTalkFontSize" class="form-label small mb-1 fw-semibold">Tamaño de fuente</label>';
$html .= ' <input type="text" id="ctaLetsTalkFontSize" name="typography[font_size]" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($fontSize) . '" placeholder="1rem">';
$html .= ' </div>';
// Select: Font Weight
$fontWeight = $this->renderer->getFieldValue($componentId, 'typography', 'font_weight', '600');
$html .= ' <div class="mb-2">';
$html .= ' <label for="ctaLetsTalkFontWeight" class="form-label small mb-1 fw-semibold">Peso de fuente</label>';
$html .= ' <select id="ctaLetsTalkFontWeight" name="typography[font_weight]" class="form-select form-select-sm">';
$html .= ' <option value="400" ' . selected($fontWeight, '400', false) . '>Normal (400)</option>';
$html .= ' <option value="500" ' . selected($fontWeight, '500', false) . '>Medium (500)</option>';
$html .= ' <option value="600" ' . selected($fontWeight, '600', false) . '>Semibold (600)</option>';
$html .= ' <option value="700" ' . selected($fontWeight, '700', false) . '>Bold (700)</option>';
$html .= ' </select>';
$html .= ' </div>';
// Select: Text Transform
$textTransform = $this->renderer->getFieldValue($componentId, 'typography', 'text_transform', 'none');
$html .= ' <div class="mb-0">';
$html .= ' <label for="ctaLetsTalkTextTransform" class="form-label small mb-1 fw-semibold">Transformación de texto</label>';
$html .= ' <select id="ctaLetsTalkTextTransform" name="typography[text_transform]" class="form-select form-select-sm">';
$html .= ' <option value="none" ' . selected($textTransform, 'none', false) . '>Normal</option>';
$html .= ' <option value="uppercase" ' . selected($textTransform, 'uppercase', false) . '>MAYÚSCULAS</option>';
$html .= ' <option value="lowercase" ' . selected($textTransform, 'lowercase', false) . '>minúsculas</option>';
$html .= ' <option value="capitalize" ' . selected($textTransform, 'capitalize', false) . '>Capitalizado</option>';
$html .= ' </select>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildColorsGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
$html .= ' Colores';
$html .= ' </h5>';
// Color: Background
$bgColor = $this->renderer->getFieldValue($componentId, 'colors', 'background_color', '#FF8600');
$html .= ' <div class="mb-2">';
$html .= ' <label for="ctaLetsTalkBgColor" class="form-label small mb-1 fw-semibold">Color de fondo</label>';
$html .= ' <input type="color" id="ctaLetsTalkBgColor" name="colors[background_color]" class="form-control form-control-color w-100" ';
$html .= ' value="' . esc_attr($bgColor) . '">';
$html .= ' </div>';
// Color: Background Hover
$bgHoverColor = $this->renderer->getFieldValue($componentId, 'colors', 'background_hover_color', '#FF6B35');
$html .= ' <div class="mb-2">';
$html .= ' <label for="ctaLetsTalkBgHoverColor" class="form-label small mb-1 fw-semibold">Color de fondo (hover)</label>';
$html .= ' <input type="color" id="ctaLetsTalkBgHoverColor" name="colors[background_hover_color]" class="form-control form-control-color w-100" ';
$html .= ' value="' . esc_attr($bgHoverColor) . '">';
$html .= ' </div>';
// Color: Text
$textColor = $this->renderer->getFieldValue($componentId, 'colors', 'text_color', '#FFFFFF');
$html .= ' <div class="mb-2">';
$html .= ' <label for="ctaLetsTalkTextColor" class="form-label small mb-1 fw-semibold">Color del texto</label>';
$html .= ' <input type="color" id="ctaLetsTalkTextColor" name="colors[text_color]" class="form-control form-control-color w-100" ';
$html .= ' value="' . esc_attr($textColor) . '">';
$html .= ' </div>';
// Color: Text Hover
$textHoverColor = $this->renderer->getFieldValue($componentId, 'colors', 'text_hover_color', '#FFFFFF');
$html .= ' <div class="mb-2">';
$html .= ' <label for="ctaLetsTalkTextHoverColor" class="form-label small mb-1 fw-semibold">Color del texto (hover)</label>';
$html .= ' <input type="color" id="ctaLetsTalkTextHoverColor" name="colors[text_hover_color]" class="form-control form-control-color w-100" ';
$html .= ' value="' . esc_attr($textHoverColor) . '">';
$html .= ' </div>';
// Text: Border Color (permite transparent)
$borderColor = $this->renderer->getFieldValue($componentId, 'colors', 'border_color', 'transparent');
$html .= ' <div class="mb-0">';
$html .= ' <label for="ctaLetsTalkBorderColor" class="form-label small mb-1 fw-semibold">Color del borde</label>';
$html .= ' <input type="text" id="ctaLetsTalkBorderColor" name="colors[border_color]" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($borderColor) . '" placeholder="transparent o #RRGGBB">';
$html .= ' <small class="text-muted">Usa "transparent" para sin borde visible</small>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildSpacingGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-arrows-angle-expand me-2" style="color: #FF8600;"></i>';
$html .= ' Espaciado';
$html .= ' </h5>';
// Text: Padding Top/Bottom
$paddingTB = $this->renderer->getFieldValue($componentId, 'spacing', 'padding_top_bottom', '0.5rem');
$html .= ' <div class="mb-2">';
$html .= ' <label for="ctaLetsTalkPaddingTB" class="form-label small mb-1 fw-semibold">Padding vertical</label>';
$html .= ' <input type="text" id="ctaLetsTalkPaddingTB" name="spacing[padding_top_bottom]" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($paddingTB) . '" placeholder="0.5rem">';
$html .= ' </div>';
// Text: Padding Left/Right
$paddingLR = $this->renderer->getFieldValue($componentId, 'spacing', 'padding_left_right', '1.5rem');
$html .= ' <div class="mb-2">';
$html .= ' <label for="ctaLetsTalkPaddingLR" class="form-label small mb-1 fw-semibold">Padding horizontal</label>';
$html .= ' <input type="text" id="ctaLetsTalkPaddingLR" name="spacing[padding_left_right]" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($paddingLR) . '" placeholder="1.5rem">';
$html .= ' </div>';
// Text: Margin Left
$marginLeft = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_left', '1rem');
$html .= ' <div class="mb-2">';
$html .= ' <label for="ctaLetsTalkMarginLeft" class="form-label small mb-1 fw-semibold">Margen izquierdo (desktop)</label>';
$html .= ' <input type="text" id="ctaLetsTalkMarginLeft" name="spacing[margin_left]" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($marginLeft) . '" placeholder="1rem">';
$html .= ' <small class="text-muted">Separación del menú en pantallas ≥992px</small>';
$html .= ' </div>';
// Text: Icon Spacing
$iconSpacing = $this->renderer->getFieldValue($componentId, 'spacing', 'icon_spacing', '0.5rem');
$html .= ' <div class="mb-0">';
$html .= ' <label for="ctaLetsTalkIconSpacing" class="form-label small mb-1 fw-semibold">Espaciado del ícono</label>';
$html .= ' <input type="text" id="ctaLetsTalkIconSpacing" name="spacing[icon_spacing]" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($iconSpacing) . '" placeholder="0.5rem">';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildVisualEffectsGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-stars me-2" style="color: #FF8600;"></i>';
$html .= ' Efectos Visuales';
$html .= ' </h5>';
// Text: Border Radius
$borderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_radius', '6px');
$html .= ' <div class="mb-2">';
$html .= ' <label for="ctaLetsTalkBorderRadius" class="form-label small mb-1 fw-semibold">Radio de bordes</label>';
$html .= ' <input type="text" id="ctaLetsTalkBorderRadius" name="visual_effects[border_radius]" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($borderRadius) . '" placeholder="6px">';
$html .= ' </div>';
// Text: Border Width
$borderWidth = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_width', '0');
$html .= ' <div class="mb-2">';
$html .= ' <label for="ctaLetsTalkBorderWidth" class="form-label small mb-1 fw-semibold">Grosor del borde</label>';
$html .= ' <input type="text" id="ctaLetsTalkBorderWidth" name="visual_effects[border_width]" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($borderWidth) . '" placeholder="0">';
$html .= ' </div>';
// Text: Box Shadow
$boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', 'none');
$html .= ' <div class="mb-2">';
$html .= ' <label for="ctaLetsTalkBoxShadow" class="form-label small mb-1 fw-semibold">Sombra</label>';
$html .= ' <input type="text" id="ctaLetsTalkBoxShadow" name="visual_effects[box_shadow]" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($boxShadow) . '" placeholder="none">';
$html .= ' <small class="text-muted">Ej: 0 2px 4px rgba(0,0,0,0.1)</small>';
$html .= ' </div>';
// Text: Transition Duration
$transitionDuration = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s');
$html .= ' <div class="mb-0">';
$html .= ' <label for="ctaLetsTalkTransition" class="form-label small mb-1 fw-semibold">Duración de transición</label>';
$html .= ' <input type="text" id="ctaLetsTalkTransition" name="visual_effects[transition_duration]" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($transitionDuration) . '" placeholder="0.3s">';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
}

View File

@@ -0,0 +1,440 @@
<?php
declare(strict_types=1);
namespace ROITheme\Admin\CtaPost\Infrastructure\Ui;
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
/**
* FormBuilder para CTA Post
*
* @package ROITheme\Admin\CtaPost\Infrastructure\Ui
*/
final class CtaPostFormBuilder
{
public function __construct(
private AdminDashboardRenderer $renderer
) {}
public function buildForm(string $componentId): string
{
$html = '';
$html .= $this->buildHeader($componentId);
$html .= '<div class="row g-3">';
// Columna izquierda
$html .= '<div class="col-lg-6">';
$html .= $this->buildVisibilityGroup($componentId);
$html .= $this->buildContentGroup($componentId);
$html .= $this->buildTypographyGroup($componentId);
$html .= '</div>';
// Columna derecha
$html .= '<div class="col-lg-6">';
$html .= $this->buildColorsGroup($componentId);
$html .= $this->buildSpacingGroup($componentId);
$html .= $this->buildEffectsGroup($componentId);
$html .= '</div>';
$html .= '</div>';
return $html;
}
private function buildHeader(string $componentId): string
{
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
$html .= ' <div>';
$html .= ' <h3 class="h4 mb-1 fw-bold">';
$html .= ' <i class="bi bi-megaphone-fill me-2" style="color: #FF8600;"></i>';
$html .= ' Configuracion de CTA Post';
$html .= ' </h3>';
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
$html .= ' CTA promocional debajo del contenido del post';
$html .= ' </p>';
$html .= ' </div>';
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="cta_post">';
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
$html .= ' Restaurar valores por defecto';
$html .= ' </button>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildVisibilityGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
$html .= ' Visibilidad';
$html .= ' </h5>';
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
$html .= $this->buildSwitch('ctaPostEnabled', 'Activar componente', 'bi-power', $enabled);
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
$html .= $this->buildSwitch('ctaPostShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop);
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
$html .= $this->buildSwitch('ctaPostShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
$html .= ' <div class="mb-0 mt-3">';
$html .= ' <label for="ctaPostShowOnPages" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
$html .= ' Mostrar en';
$html .= ' </label>';
$html .= ' <select id="ctaPostShowOnPages" class="form-select form-select-sm">';
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
$html .= ' </select>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildContentGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-card-text me-2" style="color: #FF8600;"></i>';
$html .= ' Contenido';
$html .= ' </h5>';
// Title
$title = $this->renderer->getFieldValue($componentId, 'content', 'title', 'Accede a 200,000+ Analisis de Precios Unitarios');
$html .= ' <div class="mb-3">';
$html .= ' <label for="ctaPostTitle" class="form-label small mb-1 fw-semibold">Titulo</label>';
$html .= ' <input type="text" id="ctaPostTitle" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($title) . '">';
$html .= ' </div>';
// Description
$description = $this->renderer->getFieldValue($componentId, 'content', 'description', '');
$html .= ' <div class="mb-3">';
$html .= ' <label for="ctaPostDescription" class="form-label small mb-1 fw-semibold">Descripcion</label>';
$html .= ' <textarea id="ctaPostDescription" class="form-control form-control-sm" rows="3">';
$html .= esc_textarea($description);
$html .= '</textarea>';
$html .= ' </div>';
// Button Text
$buttonText = $this->renderer->getFieldValue($componentId, 'content', 'button_text', 'Ver Catalogo Completo');
$html .= ' <div class="mb-3">';
$html .= ' <label for="ctaPostButtonText" class="form-label small mb-1 fw-semibold">Texto del boton</label>';
$html .= ' <input type="text" id="ctaPostButtonText" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($buttonText) . '">';
$html .= ' </div>';
// Button URL
$buttonUrl = $this->renderer->getFieldValue($componentId, 'content', 'button_url', '/catalogo');
$html .= ' <div class="mb-3">';
$html .= ' <label for="ctaPostButtonUrl" class="form-label small mb-1 fw-semibold">URL del boton</label>';
$html .= ' <input type="text" id="ctaPostButtonUrl" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($buttonUrl) . '">';
$html .= ' </div>';
// Button Icon
$buttonIcon = $this->renderer->getFieldValue($componentId, 'content', 'button_icon', 'bi-arrow-right');
$html .= ' <div class="mb-0">';
$html .= ' <label for="ctaPostButtonIcon" class="form-label small mb-1 fw-semibold">Icono del boton</label>';
$html .= ' <input type="text" id="ctaPostButtonIcon" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($buttonIcon) . '" placeholder="bi-arrow-right">';
$html .= ' <small class="text-muted">Clase de Bootstrap Icons</small>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildTypographyGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-fonts me-2" style="color: #FF8600;"></i>';
$html .= ' Tipografia';
$html .= ' </h5>';
$html .= ' <div class="row g-2 mb-3">';
$titleFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_size', '1.5rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="ctaPostTitleFontSize" class="form-label small mb-1 fw-semibold">Tamano titulo</label>';
$html .= ' <input type="text" id="ctaPostTitleFontSize" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($titleFontSize) . '">';
$html .= ' </div>';
$titleFontWeight = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_weight', '700');
$html .= ' <div class="col-6">';
$html .= ' <label for="ctaPostTitleFontWeight" class="form-label small mb-1 fw-semibold">Peso titulo</label>';
$html .= ' <input type="text" id="ctaPostTitleFontWeight" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($titleFontWeight) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-0">';
$descFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'description_font_size', '1rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="ctaPostDescFontSize" class="form-label small mb-1 fw-semibold">Tamano descripcion</label>';
$html .= ' <input type="text" id="ctaPostDescFontSize" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($descFontSize) . '">';
$html .= ' </div>';
$buttonFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'button_font_size', '1.125rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="ctaPostButtonFontSize" class="form-label small mb-1 fw-semibold">Tamano boton</label>';
$html .= ' <input type="text" id="ctaPostButtonFontSize" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($buttonFontSize) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildColorsGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
$html .= ' Colores';
$html .= ' </h5>';
// Gradiente
$html .= ' <p class="small fw-semibold mb-2">Gradiente de fondo</p>';
$html .= ' <div class="row g-2 mb-3">';
$gradientStart = $this->renderer->getFieldValue($componentId, 'colors', 'gradient_start', '#FF8600');
$html .= $this->buildColorPicker('ctaPostGradientStart', 'Inicio', $gradientStart);
$gradientEnd = $this->renderer->getFieldValue($componentId, 'colors', 'gradient_end', '#FFB800');
$html .= $this->buildColorPicker('ctaPostGradientEnd', 'Fin', $gradientEnd);
$html .= ' </div>';
// Textos
$html .= ' <p class="small fw-semibold mb-2">Textos</p>';
$html .= ' <div class="row g-2 mb-3">';
$titleColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_color', '#ffffff');
$html .= $this->buildColorPicker('ctaPostTitleColor', 'Titulo', $titleColor);
$descColor = $this->renderer->getFieldValue($componentId, 'colors', 'description_color', '#ffffff');
$html .= $this->buildColorPicker('ctaPostDescColor', 'Descripcion', $descColor);
$html .= ' </div>';
// Boton
$html .= ' <p class="small fw-semibold mb-2">Boton</p>';
$html .= ' <div class="row g-2 mb-3">';
$buttonBg = $this->renderer->getFieldValue($componentId, 'colors', 'button_bg_color', '#ffffff');
$html .= $this->buildColorPicker('ctaPostButtonBg', 'Fondo', $buttonBg);
$buttonText = $this->renderer->getFieldValue($componentId, 'colors', 'button_text_color', '#212529');
$html .= $this->buildColorPicker('ctaPostButtonText', 'Texto', $buttonText);
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-0">';
$buttonHoverBg = $this->renderer->getFieldValue($componentId, 'colors', 'button_hover_bg', '#f8f9fa');
$html .= $this->buildColorPicker('ctaPostButtonHoverBg', 'Hover', $buttonHoverBg);
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildSpacingGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-arrows-move me-2" style="color: #FF8600;"></i>';
$html .= ' Espaciado';
$html .= ' </h5>';
$html .= ' <div class="row g-2 mb-3">';
$marginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'container_margin_top', '3rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="ctaPostMarginTop" class="form-label small mb-1 fw-semibold">Margen superior</label>';
$html .= ' <input type="text" id="ctaPostMarginTop" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($marginTop) . '">';
$html .= ' </div>';
$marginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'container_margin_bottom', '3rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="ctaPostMarginBottom" class="form-label small mb-1 fw-semibold">Margen inferior</label>';
$html .= ' <input type="text" id="ctaPostMarginBottom" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($marginBottom) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-0">';
$padding = $this->renderer->getFieldValue($componentId, 'spacing', 'container_padding', '1.5rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="ctaPostPadding" class="form-label small mb-1 fw-semibold">Padding interno</label>';
$html .= ' <input type="text" id="ctaPostPadding" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($padding) . '">';
$html .= ' </div>';
$titleMargin = $this->renderer->getFieldValue($componentId, 'spacing', 'title_margin_bottom', '0.5rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="ctaPostTitleMargin" class="form-label small mb-1 fw-semibold">Margen titulo</label>';
$html .= ' <input type="text" id="ctaPostTitleMargin" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($titleMargin) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildEffectsGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-magic me-2" style="color: #FF8600;"></i>';
$html .= ' Efectos Visuales';
$html .= ' </h5>';
$html .= ' <div class="row g-2 mb-3">';
$borderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_radius', '0.375rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="ctaPostBorderRadius" class="form-label small mb-1 fw-semibold">Radio contenedor</label>';
$html .= ' <input type="text" id="ctaPostBorderRadius" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($borderRadius) . '">';
$html .= ' </div>';
$gradientAngle = $this->renderer->getFieldValue($componentId, 'visual_effects', 'gradient_angle', '135deg');
$html .= ' <div class="col-6">';
$html .= ' <label for="ctaPostGradientAngle" class="form-label small mb-1 fw-semibold">Angulo gradiente</label>';
$html .= ' <input type="text" id="ctaPostGradientAngle" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($gradientAngle) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-3">';
$buttonRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_border_radius', '0.375rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="ctaPostButtonRadius" class="form-label small mb-1 fw-semibold">Radio boton</label>';
$html .= ' <input type="text" id="ctaPostButtonRadius" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($buttonRadius) . '">';
$html .= ' </div>';
$buttonPadding = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_padding', '0.5rem 1rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="ctaPostButtonPadding" class="form-label small mb-1 fw-semibold">Padding boton</label>';
$html .= ' <input type="text" id="ctaPostButtonPadding" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($buttonPadding) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-0">';
$transition = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s');
$html .= ' <div class="col-6">';
$html .= ' <label for="ctaPostTransition" class="form-label small mb-1 fw-semibold">Transicion</label>';
$html .= ' <input type="text" id="ctaPostTransition" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($transition) . '">';
$html .= ' </div>';
$boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', 'none');
$html .= ' <div class="col-6">';
$html .= ' <label for="ctaPostBoxShadow" class="form-label small mb-1 fw-semibold">Sombra</label>';
$html .= ' <input type="text" id="ctaPostBoxShadow" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($boxShadow) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildSwitch(string $id, string $label, string $icon, mixed $checked): string
{
$checked = $checked === true || $checked === '1' || $checked === 1;
$html = ' <div class="mb-2">';
$html .= ' <div class="form-check form-switch">';
$html .= sprintf(
' <input class="form-check-input" type="checkbox" id="%s" %s>',
esc_attr($id),
$checked ? 'checked' : ''
);
$html .= sprintf(
' <label class="form-check-label small" for="%s">',
esc_attr($id)
);
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
$html .= sprintf(' <strong>%s</strong>', esc_html($label));
$html .= ' </label>';
$html .= ' </div>';
$html .= ' </div>';
return $html;
}
private function buildColorPicker(string $id, string $label, string $value): string
{
$html = ' <div class="col-6">';
$html .= sprintf(
' <label class="form-label small fw-semibold">%s</label>',
esc_html($label)
);
$html .= ' <div class="input-group input-group-sm">';
$html .= sprintf(
' <input type="color" class="form-control form-control-color" id="%s" value="%s">',
esc_attr($id),
esc_attr($value)
);
$html .= sprintf(
' <span class="input-group-text" id="%sValue">%s</span>',
esc_attr($id),
esc_html(strtoupper($value))
);
$html .= ' </div>';
$html .= ' </div>';
return $html;
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace ROITheme\Admin\Domain\Contracts;
/**
* Contrato para tabs de componentes en el dashboard
*
* Domain - Lógica pura sin WordPress
*/
interface ComponentTabInterface
{
/**
* Obtiene el ID del componente
*
* @return string
*/
public function getId(): string;
/**
* Obtiene el nombre visible del tab
*
* @return string
*/
public function getLabel(): string;
/**
* Obtiene el ícono del tab
*
* @return string
*/
public function getIcon(): string;
/**
* Renderiza el contenido del tab
*
* @return string HTML del contenido
*/
public function renderContent(): string;
/**
* Verifica si el tab está activo
*
* @return bool
*/
public function isActive(): bool;
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace ROITheme\Admin\Domain\Contracts;
/**
* Contrato para renderizar el dashboard del panel de administración
*
* Domain - Lógica pura sin WordPress
*/
interface DashboardRendererInterface
{
/**
* Renderiza el dashboard completo
*
* @return string HTML del dashboard
*/
public function render(): string;
/**
* Verifica si el renderizador soporta un tipo de vista
*
* @param string $viewType Tipo de vista (dashboard, settings, etc.)
* @return bool
*/
public function supports(string $viewType): bool;
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace ROITheme\Admin\Domain\Contracts;
/**
* Contrato para registrar el menú de administración
*
* Domain - Lógica pura sin WordPress
*/
interface MenuRegistrarInterface
{
/**
* Registra el menú en el sistema de administración
*
* @return void
*/
public function register(): void;
/**
* Obtiene la capacidad requerida para acceder al menú
*
* @return string
*/
public function getCapability(): string;
/**
* Obtiene el slug del menú
*
* @return string
*/
public function getSlug(): string;
}

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace ROITheme\Admin\Domain\ValueObjects;
/**
* Value Object para representar un item del menú
*
* Domain - Objeto inmutable sin WordPress
*/
final class MenuItem
{
/**
* @param string $pageTitle Título de la página
* @param string $menuTitle Título del menú
* @param string $capability Capacidad requerida
* @param string $menuSlug Slug del menú
* @param string $icon Ícono del menú
* @param int $position Posición en el menú
*/
public function __construct(
private readonly string $pageTitle,
private readonly string $menuTitle,
private readonly string $capability,
private readonly string $menuSlug,
private readonly string $icon,
private readonly int $position
) {
$this->validate();
}
private function validate(): void
{
if (empty($this->pageTitle)) {
throw new \InvalidArgumentException('Page title cannot be empty');
}
if (empty($this->menuTitle)) {
throw new \InvalidArgumentException('Menu title cannot be empty');
}
if (empty($this->capability)) {
throw new \InvalidArgumentException('Capability cannot be empty');
}
if (empty($this->menuSlug)) {
throw new \InvalidArgumentException('Menu slug cannot be empty');
}
if ($this->position < 0) {
throw new \InvalidArgumentException('Position must be >= 0');
}
}
public function getPageTitle(): string
{
return $this->pageTitle;
}
public function getMenuTitle(): string
{
return $this->menuTitle;
}
public function getCapability(): string
{
return $this->capability;
}
public function getMenuSlug(): string
{
return $this->menuSlug;
}
public function getIcon(): string
{
return $this->icon;
}
public function getPosition(): int
{
return $this->position;
}
}

View File

@@ -0,0 +1,280 @@
<?php
declare(strict_types=1);
namespace ROITheme\Admin\FeaturedImage\Infrastructure\Ui;
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
final class FeaturedImageFormBuilder
{
public function __construct(
private AdminDashboardRenderer $renderer
) {}
public function buildForm(string $componentId): string
{
$html = '';
$html .= $this->buildHeader($componentId);
$html .= '<div class="row g-3">';
$html .= ' <div class="col-lg-6">';
$html .= $this->buildVisibilityGroup($componentId);
$html .= $this->buildContentGroup($componentId);
$html .= ' </div>';
$html .= ' <div class="col-lg-6">';
$html .= $this->buildSpacingGroup($componentId);
$html .= $this->buildEffectsGroup($componentId);
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildHeader(string $componentId): string
{
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
$html .= ' <div>';
$html .= ' <h3 class="h4 mb-1 fw-bold">';
$html .= ' <i class="bi bi-image me-2" style="color: #FF8600;"></i>';
$html .= ' Configuracion de Imagen Destacada';
$html .= ' </h3>';
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
$html .= ' Personaliza la imagen destacada de los posts';
$html .= ' </p>';
$html .= ' </div>';
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="featured_image">';
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
$html .= ' Restaurar valores por defecto';
$html .= ' </button>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildVisibilityGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
$html .= ' Visibilidad';
$html .= ' </h5>';
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
$html .= ' <div class="mb-2">';
$html .= ' <div class="form-check form-switch">';
$html .= ' <input class="form-check-input" type="checkbox" id="featuredImageEnabled" ';
$html .= checked($enabled, true, false) . '>';
$html .= ' <label class="form-check-label small" for="featuredImageEnabled">';
$html .= ' <i class="bi bi-power me-1" style="color: #FF8600;"></i>';
$html .= ' <strong>Mostrar imagen destacada</strong>';
$html .= ' </label>';
$html .= ' </div>';
$html .= ' </div>';
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
$html .= ' <div class="mb-2">';
$html .= ' <div class="form-check form-switch">';
$html .= ' <input class="form-check-input" type="checkbox" id="featuredImageShowOnDesktop" ';
$html .= checked($showOnDesktop, true, false) . '>';
$html .= ' <label class="form-check-label small" for="featuredImageShowOnDesktop">';
$html .= ' <i class="bi bi-display me-1" style="color: #FF8600;"></i>';
$html .= ' <strong>Mostrar en Desktop</strong>';
$html .= ' </label>';
$html .= ' </div>';
$html .= ' </div>';
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
$html .= ' <div class="mb-2">';
$html .= ' <div class="form-check form-switch">';
$html .= ' <input class="form-check-input" type="checkbox" id="featuredImageShowOnMobile" ';
$html .= checked($showOnMobile, true, false) . '>';
$html .= ' <label class="form-check-label small" for="featuredImageShowOnMobile">';
$html .= ' <i class="bi bi-phone me-1" style="color: #FF8600;"></i>';
$html .= ' <strong>Mostrar en Mobile</strong>';
$html .= ' </label>';
$html .= ' </div>';
$html .= ' </div>';
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
$html .= ' <div class="mb-0 mt-3">';
$html .= ' <label for="featuredImageShowOnPages" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
$html .= ' Mostrar en';
$html .= ' </label>';
$html .= ' <select id="featuredImageShowOnPages" class="form-select form-select-sm">';
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las paginas</option>';
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo paginas</option>';
$html .= ' </select>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildContentGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-card-image me-2" style="color: #FF8600;"></i>';
$html .= ' Contenido';
$html .= ' </h5>';
$imageSize = $this->renderer->getFieldValue($componentId, 'content', 'image_size', 'roi-featured-large');
$html .= ' <div class="mb-3">';
$html .= ' <label for="featuredImageSize" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-aspect-ratio me-1" style="color: #FF8600;"></i>';
$html .= ' Tamano de imagen';
$html .= ' </label>';
$html .= ' <select id="featuredImageSize" class="form-select form-select-sm">';
$html .= ' <option value="roi-featured-large" ' . selected($imageSize, 'roi-featured-large', false) . '>Grande (1200x600)</option>';
$html .= ' <option value="roi-featured-medium" ' . selected($imageSize, 'roi-featured-medium', false) . '>Mediano (800x400)</option>';
$html .= ' <option value="full" ' . selected($imageSize, 'full', false) . '>Original (tamano completo)</option>';
$html .= ' </select>';
$html .= ' </div>';
$lazyLoading = $this->renderer->getFieldValue($componentId, 'content', 'lazy_loading', true);
$html .= ' <div class="mb-2">';
$html .= ' <div class="form-check form-switch">';
$html .= ' <input class="form-check-input" type="checkbox" id="featuredImageLazyLoading" ';
$html .= checked($lazyLoading, true, false) . '>';
$html .= ' <label class="form-check-label small" for="featuredImageLazyLoading">';
$html .= ' <i class="bi bi-lightning me-1" style="color: #FF8600;"></i>';
$html .= ' <strong>Carga diferida (lazy loading)</strong>';
$html .= ' </label>';
$html .= ' </div>';
$html .= ' <small class="text-muted">Mejora rendimiento cargando imagen cuando es visible</small>';
$html .= ' </div>';
$linkToMedia = $this->renderer->getFieldValue($componentId, 'content', 'link_to_media', false);
$html .= ' <div class="mb-0">';
$html .= ' <div class="form-check form-switch">';
$html .= ' <input class="form-check-input" type="checkbox" id="featuredImageLinkToMedia" ';
$html .= checked($linkToMedia, true, false) . '>';
$html .= ' <label class="form-check-label small" for="featuredImageLinkToMedia">';
$html .= ' <i class="bi bi-link-45deg me-1" style="color: #FF8600;"></i>';
$html .= ' <strong>Enlazar a imagen completa</strong>';
$html .= ' </label>';
$html .= ' </div>';
$html .= ' <small class="text-muted">Abre la imagen en tamano completo al hacer clic</small>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildSpacingGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-arrows-move me-2" style="color: #FF8600;"></i>';
$html .= ' Espaciado';
$html .= ' </h5>';
$html .= ' <div class="row g-2 mb-0">';
$marginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_top', '1rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="featuredImageMarginTop" class="form-label small mb-1 fw-semibold">';
$html .= ' Margen superior';
$html .= ' </label>';
$html .= ' <input type="text" id="featuredImageMarginTop" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($marginTop) . '" placeholder="1rem">';
$html .= ' </div>';
$marginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_bottom', '2rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="featuredImageMarginBottom" class="form-label small mb-1 fw-semibold">';
$html .= ' Margen inferior';
$html .= ' </label>';
$html .= ' <input type="text" id="featuredImageMarginBottom" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($marginBottom) . '" placeholder="2rem">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildEffectsGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-magic me-2" style="color: #FF8600;"></i>';
$html .= ' Efectos Visuales';
$html .= ' </h5>';
$borderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_radius', '12px');
$html .= ' <div class="mb-2">';
$html .= ' <label for="featuredImageBorderRadius" class="form-label small mb-1 fw-semibold">';
$html .= ' Radio de bordes';
$html .= ' </label>';
$html .= ' <input type="text" id="featuredImageBorderRadius" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($borderRadius) . '" placeholder="12px">';
$html .= ' </div>';
$boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', '0 8px 24px rgba(0, 0, 0, 0.1)');
$html .= ' <div class="mb-3">';
$html .= ' <label for="featuredImageBoxShadow" class="form-label small mb-1 fw-semibold">';
$html .= ' Sombra';
$html .= ' </label>';
$html .= ' <input type="text" id="featuredImageBoxShadow" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($boxShadow) . '">';
$html .= ' </div>';
$hoverEffect = $this->renderer->getFieldValue($componentId, 'visual_effects', 'hover_effect', true);
$html .= ' <div class="mb-2">';
$html .= ' <div class="form-check form-switch">';
$html .= ' <input class="form-check-input" type="checkbox" id="featuredImageHoverEffect" ';
$html .= checked($hoverEffect, true, false) . '>';
$html .= ' <label class="form-check-label small" for="featuredImageHoverEffect">';
$html .= ' <i class="bi bi-hand-index me-1" style="color: #FF8600;"></i>';
$html .= ' <strong>Efecto hover</strong>';
$html .= ' </label>';
$html .= ' </div>';
$html .= ' <small class="text-muted">Aplica efecto de escala sutil al pasar el mouse</small>';
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-0">';
$hoverScale = $this->renderer->getFieldValue($componentId, 'visual_effects', 'hover_scale', '1.02');
$html .= ' <div class="col-6">';
$html .= ' <label for="featuredImageHoverScale" class="form-label small mb-1 fw-semibold">';
$html .= ' Escala en hover';
$html .= ' </label>';
$html .= ' <input type="text" id="featuredImageHoverScale" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($hoverScale) . '" placeholder="1.02">';
$html .= ' </div>';
$transitionDuration = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s');
$html .= ' <div class="col-6">';
$html .= ' <label for="featuredImageTransitionDuration" class="form-label small mb-1 fw-semibold">';
$html .= ' Duracion transicion';
$html .= ' </label>';
$html .= ' <input type="text" id="featuredImageTransitionDuration" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($transitionDuration) . '" placeholder="0.3s">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
}

View File

@@ -0,0 +1,413 @@
<?php
declare(strict_types=1);
namespace ROITheme\Admin\Footer\Infrastructure\Ui;
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
/**
* FormBuilder para Footer
*
* RESPONSABILIDAD: Generar formulario de configuracion del Footer
*
* @package ROITheme\Admin\Footer\Infrastructure\Ui
*/
final class FooterFormBuilder
{
public function __construct(
private AdminDashboardRenderer $renderer
) {}
public function buildForm(string $componentId): string
{
$html = '';
$html .= $this->buildHeader($componentId);
$html .= '<div class="row g-3">';
// Columna izquierda
$html .= '<div class="col-lg-6">';
$html .= $this->buildVisibilityGroup($componentId);
$html .= $this->buildWidget1Group($componentId);
$html .= $this->buildWidget2Group($componentId);
$html .= $this->buildWidget3Group($componentId);
$html .= $this->buildNewsletterGroup($componentId);
$html .= '</div>';
// Columna derecha
$html .= '<div class="col-lg-6">';
$html .= $this->buildFooterBottomGroup($componentId);
$html .= $this->buildColorsGroup($componentId);
$html .= $this->buildSpacingGroup($componentId);
$html .= $this->buildEffectsGroup($componentId);
$html .= '</div>';
$html .= '</div>';
return $html;
}
private function buildHeader(string $componentId): string
{
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
$html .= ' <div>';
$html .= ' <h3 class="h4 mb-1 fw-bold">';
$html .= ' <i class="bi bi-layout-text-window-reverse me-2" style="color: #FF8600;"></i>';
$html .= ' Configuracion de Footer';
$html .= ' </h3>';
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
$html .= ' Footer con menus de navegacion y newsletter';
$html .= ' </p>';
$html .= ' </div>';
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="footer">';
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
$html .= ' Restaurar valores por defecto';
$html .= ' </button>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildVisibilityGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
$html .= ' Visibilidad';
$html .= ' </h5>';
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
$html .= $this->buildSwitch('footerEnabled', 'Activar componente', 'bi-power', $enabled);
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
$html .= $this->buildSwitch('footerShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop);
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
$html .= $this->buildSwitch('footerShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildWidget1Group(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-list-ul me-2" style="color: #FF8600;"></i>';
$html .= ' Widget 1 (Menu)';
$html .= ' </h5>';
$visible = $this->renderer->getFieldValue($componentId, 'widget_1', 'widget_1_visible', true);
$html .= $this->buildSwitch('footerWidget1Visible', 'Mostrar Widget 1', 'bi-eye', $visible);
$title = $this->renderer->getFieldValue($componentId, 'widget_1', 'widget_1_title', 'Recursos');
$html .= $this->buildTextInput('footerWidget1Title', 'Titulo', 'bi-type', $title);
$html .= ' <div class="alert alert-info small mb-0 mt-2">';
$html .= ' <i class="bi bi-info-circle me-1"></i>';
$html .= ' El contenido se gestiona desde <strong>Apariencia &gt; Menus &gt; Footer Menu 1</strong>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildWidget2Group(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-list-ul me-2" style="color: #FF8600;"></i>';
$html .= ' Widget 2 (Menu)';
$html .= ' </h5>';
$visible = $this->renderer->getFieldValue($componentId, 'widget_2', 'widget_2_visible', true);
$html .= $this->buildSwitch('footerWidget2Visible', 'Mostrar Widget 2', 'bi-eye', $visible);
$title = $this->renderer->getFieldValue($componentId, 'widget_2', 'widget_2_title', 'Soporte');
$html .= $this->buildTextInput('footerWidget2Title', 'Titulo', 'bi-type', $title);
$html .= ' <div class="alert alert-info small mb-0 mt-2">';
$html .= ' <i class="bi bi-info-circle me-1"></i>';
$html .= ' El contenido se gestiona desde <strong>Apariencia &gt; Menus &gt; Footer Menu 2</strong>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildWidget3Group(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-list-ul me-2" style="color: #FF8600;"></i>';
$html .= ' Widget 3 (Menu)';
$html .= ' </h5>';
$visible = $this->renderer->getFieldValue($componentId, 'widget_3', 'widget_3_visible', true);
$html .= $this->buildSwitch('footerWidget3Visible', 'Mostrar Widget 3', 'bi-eye', $visible);
$title = $this->renderer->getFieldValue($componentId, 'widget_3', 'widget_3_title', 'Empresa');
$html .= $this->buildTextInput('footerWidget3Title', 'Titulo', 'bi-type', $title);
$html .= ' <div class="alert alert-info small mb-0 mt-2">';
$html .= ' <i class="bi bi-info-circle me-1"></i>';
$html .= ' El contenido se gestiona desde <strong>Apariencia &gt; Menus &gt; Footer Menu 3</strong>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildNewsletterGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-envelope-paper me-2" style="color: #FF8600;"></i>';
$html .= ' Newsletter';
$html .= ' </h5>';
$visible = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_visible', true);
$html .= $this->buildSwitch('footerNewsletterVisible', 'Mostrar Newsletter', 'bi-eye', $visible);
$title = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_title', 'Suscribete al Newsletter');
$html .= $this->buildTextInput('footerNewsletterTitle', 'Titulo', 'bi-type', $title);
$description = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_description', 'Recibe las ultimas actualizaciones.');
$html .= $this->buildTextarea('footerNewsletterDescription', 'Descripcion', 'bi-text-paragraph', $description);
$placeholder = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_placeholder', 'Email');
$html .= $this->buildTextInput('footerNewsletterPlaceholder', 'Placeholder email', 'bi-input-cursor', $placeholder);
$buttonText = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_button_text', 'Suscribirse');
$html .= $this->buildTextInput('footerNewsletterButtonText', 'Texto boton', 'bi-cursor', $buttonText);
$webhookUrl = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_webhook_url', '');
$html .= $this->buildPasswordInput('footerNewsletterWebhookUrl', 'URL del Webhook', 'bi-link-45deg', $webhookUrl);
$successMsg = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_success_message', 'Gracias por suscribirte!');
$html .= $this->buildTextInput('footerNewsletterSuccessMessage', 'Mensaje exito', 'bi-check-circle', $successMsg);
$errorMsg = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_error_message', 'Error al suscribirse.');
$html .= $this->buildTextInput('footerNewsletterErrorMessage', 'Mensaje error', 'bi-x-circle', $errorMsg);
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildFooterBottomGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-c-circle me-2" style="color: #FF8600;"></i>';
$html .= ' Pie de Footer';
$html .= ' </h5>';
$copyright = $this->renderer->getFieldValue($componentId, 'footer_bottom', 'copyright_text', date('Y') . ' Todos los derechos reservados.');
$html .= $this->buildTextInput('footerCopyrightText', 'Texto copyright', 'bi-c-circle', $copyright);
$html .= ' <div class="alert alert-secondary small mb-0 mt-2">';
$html .= ' <i class="bi bi-info-circle me-1"></i>';
$html .= ' El simbolo &copy; se agrega automaticamente';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildColorsGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
$html .= ' Colores';
$html .= ' </h5>';
$bgColor = $this->renderer->getFieldValue($componentId, 'colors', 'bg_color', '#212529');
$html .= $this->buildColorInput('footerBgColor', 'Fondo footer', $bgColor);
$textColor = $this->renderer->getFieldValue($componentId, 'colors', 'text_color', '#ffffff');
$html .= $this->buildColorInput('footerTextColor', 'Color texto', $textColor);
$titleColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_color', '#ffffff');
$html .= $this->buildColorInput('footerTitleColor', 'Color titulos', $titleColor);
$linkColor = $this->renderer->getFieldValue($componentId, 'colors', 'link_color', '#ffffff');
$html .= $this->buildColorInput('footerLinkColor', 'Color links', $linkColor);
$linkHoverColor = $this->renderer->getFieldValue($componentId, 'colors', 'link_hover_color', '#FF8600');
$html .= $this->buildColorInput('footerLinkHoverColor', 'Color links hover', $linkHoverColor);
$buttonBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'button_bg_color', '#0d6efd');
$html .= $this->buildColorInput('footerButtonBgColor', 'Fondo boton', $buttonBgColor);
$buttonTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'button_text_color', '#ffffff');
$html .= $this->buildColorInput('footerButtonTextColor', 'Texto boton', $buttonTextColor);
$buttonHoverBg = $this->renderer->getFieldValue($componentId, 'colors', 'button_hover_bg', '#0b5ed7');
$html .= $this->buildColorInput('footerButtonHoverBg', 'Fondo boton hover', $buttonHoverBg);
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildSpacingGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-arrows-expand me-2" style="color: #FF8600;"></i>';
$html .= ' Espaciado';
$html .= ' </h5>';
$paddingY = $this->renderer->getFieldValue($componentId, 'spacing', 'padding_y', '3rem');
$html .= $this->buildTextInput('footerPaddingY', 'Padding vertical', 'bi-arrows-vertical', $paddingY);
$marginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_top', '0');
$html .= $this->buildTextInput('footerMarginTop', 'Margen superior', 'bi-arrow-up', $marginTop);
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildEffectsGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-stars me-2" style="color: #FF8600;"></i>';
$html .= ' Efectos Visuales';
$html .= ' </h5>';
$inputRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'input_border_radius', '6px');
$html .= $this->buildTextInput('footerInputBorderRadius', 'Radio input', 'bi-square', $inputRadius);
$buttonRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_border_radius', '6px');
$html .= $this->buildTextInput('footerButtonBorderRadius', 'Radio boton', 'bi-square', $buttonRadius);
$transition = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s');
$html .= $this->buildTextInput('footerTransitionDuration', 'Duracion transicion', 'bi-hourglass', $transition);
$html .= ' </div>';
$html .= '</div>';
return $html;
}
// Helper methods
private function buildSwitch(string $id, string $label, string $icon, $value): string
{
$checked = $value === true || $value === '1' || $value === 1 ? 'checked' : '';
$html = ' <div class="form-check form-switch mb-2">';
$html .= ' <input class="form-check-input" type="checkbox" id="' . esc_attr($id) . '" ' . $checked . '>';
$html .= ' <label class="form-check-label small" for="' . esc_attr($id) . '">';
$html .= ' <i class="bi ' . esc_attr($icon) . ' me-1" style="color: #FF8600;"></i>';
$html .= ' ' . esc_html($label);
$html .= ' </label>';
$html .= ' </div>';
return $html;
}
private function buildTextInput(string $id, string $label, string $icon, mixed $value): string
{
$value = $this->normalizeStringValue($value);
$html = ' <div class="mb-3">';
$html .= ' <label for="' . esc_attr($id) . '" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi ' . esc_attr($icon) . ' me-1" style="color: #FF8600;"></i>';
$html .= ' ' . esc_html($label);
$html .= ' </label>';
$html .= ' <input type="text" class="form-control form-control-sm" id="' . esc_attr($id) . '" value="' . esc_attr($value) . '">';
$html .= ' </div>';
return $html;
}
private function buildPasswordInput(string $id, string $label, string $icon, mixed $value): string
{
$value = $this->normalizeStringValue($value);
$html = ' <div class="mb-3">';
$html .= ' <label for="' . esc_attr($id) . '" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi ' . esc_attr($icon) . ' me-1" style="color: #FF8600;"></i>';
$html .= ' ' . esc_html($label);
$html .= ' </label>';
$html .= ' <input type="password" class="form-control form-control-sm" id="' . esc_attr($id) . '" value="' . esc_attr($value) . '">';
$html .= ' <div class="form-text small">URL oculta por seguridad</div>';
$html .= ' </div>';
return $html;
}
private function buildTextarea(string $id, string $label, string $icon, mixed $value): string
{
$value = $this->normalizeStringValue($value);
$html = ' <div class="mb-3">';
$html .= ' <label for="' . esc_attr($id) . '" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi ' . esc_attr($icon) . ' me-1" style="color: #FF8600;"></i>';
$html .= ' ' . esc_html($label);
$html .= ' </label>';
$html .= ' <textarea class="form-control form-control-sm" id="' . esc_attr($id) . '" rows="2">' . esc_textarea($value) . '</textarea>';
$html .= ' </div>';
return $html;
}
private function buildColorInput(string $id, string $label, mixed $value): string
{
$value = $this->normalizeStringValue($value);
$html = ' <div class="mb-2 d-flex align-items-center gap-2">';
$html .= ' <input type="color" class="form-control form-control-color" id="' . esc_attr($id) . '" value="' . esc_attr($value) . '" style="width: 40px; height: 30px;">';
$html .= ' <label for="' . esc_attr($id) . '" class="form-label mb-0 small">' . esc_html($label) . '</label>';
$html .= ' </div>';
return $html;
}
/**
* Normaliza un valor a string para inputs de formulario
*
* El repositorio convierte '0' a false y '1' a true automáticamente,
* pero para campos de texto necesitamos el valor original como string.
*/
private function normalizeStringValue(mixed $value): string
{
if ($value === false) {
return '0';
}
if ($value === true) {
return '1';
}
return (string) $value;
}
}

View File

@@ -0,0 +1,416 @@
<?php
declare(strict_types=1);
namespace ROITheme\Admin\Hero\Infrastructure\Ui;
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
final class HeroFormBuilder
{
public function __construct(
private AdminDashboardRenderer $renderer
) {}
public function buildForm(string $componentId): string
{
$html = '';
$html .= $this->buildHeader($componentId);
$html .= '<div class="row g-3">';
$html .= ' <div class="col-lg-6">';
$html .= $this->buildVisibilityGroup($componentId);
$html .= $this->buildContentGroup($componentId);
$html .= $this->buildEffectsGroup($componentId);
$html .= ' </div>';
$html .= ' <div class="col-lg-6">';
$html .= $this->buildColorsGroup($componentId);
$html .= $this->buildTypographyGroup($componentId);
$html .= $this->buildSpacingGroup($componentId);
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildHeader(string $componentId): string
{
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
$html .= ' <div>';
$html .= ' <h3 class="h4 mb-1 fw-bold">';
$html .= ' <i class="bi bi-image me-2" style="color: #FF8600;"></i>';
$html .= ' Configuración de Hero Section';
$html .= ' </h3>';
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
$html .= ' Personaliza la sección hero con título y badges de categorías';
$html .= ' </p>';
$html .= ' </div>';
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="hero">';
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
$html .= ' Restaurar valores por defecto';
$html .= ' </button>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildVisibilityGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
$html .= ' Visibilidad';
$html .= ' </h5>';
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
$html .= ' <div class="mb-2">';
$html .= ' <div class="form-check form-switch">';
$html .= ' <input class="form-check-input" type="checkbox" id="heroEnabled" ';
$html .= checked($enabled, true, false) . '>';
$html .= ' <label class="form-check-label small" for="heroEnabled">';
$html .= ' <i class="bi bi-power me-1" style="color: #FF8600;"></i>';
$html .= ' <strong>Activar Hero Section</strong>';
$html .= ' </label>';
$html .= ' </div>';
$html .= ' </div>';
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
$html .= ' <div class="mb-2">';
$html .= ' <div class="form-check form-switch">';
$html .= ' <input class="form-check-input" type="checkbox" id="heroShowOnDesktop" ';
$html .= checked($showOnDesktop, true, false) . '>';
$html .= ' <label class="form-check-label small" for="heroShowOnDesktop">';
$html .= ' <i class="bi bi-display me-1" style="color: #FF8600;"></i>';
$html .= ' <strong>Mostrar en Desktop</strong>';
$html .= ' </label>';
$html .= ' </div>';
$html .= ' </div>';
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
$html .= ' <div class="mb-2">';
$html .= ' <div class="form-check form-switch">';
$html .= ' <input class="form-check-input" type="checkbox" id="heroShowOnMobile" ';
$html .= checked($showOnMobile, true, false) . '>';
$html .= ' <label class="form-check-label small" for="heroShowOnMobile">';
$html .= ' <i class="bi bi-phone me-1" style="color: #FF8600;"></i>';
$html .= ' <strong>Mostrar en Mobile</strong>';
$html .= ' </label>';
$html .= ' </div>';
$html .= ' </div>';
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
$html .= ' <div class="mb-0 mt-3">';
$html .= ' <label for="heroShowOnPages" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
$html .= ' Mostrar en';
$html .= ' </label>';
$html .= ' <select id="heroShowOnPages" class="form-select form-select-sm">';
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las páginas</option>';
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo páginas</option>';
$html .= ' <option value="home" ' . selected($showOnPages, 'home', false) . '>Solo página de inicio</option>';
$html .= ' </select>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildContentGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-card-text me-2" style="color: #FF8600;"></i>';
$html .= ' Contenido';
$html .= ' </h5>';
$showCategories = $this->renderer->getFieldValue($componentId, 'content', 'show_categories', true);
$html .= ' <div class="mb-2">';
$html .= ' <div class="form-check form-switch">';
$html .= ' <input class="form-check-input" type="checkbox" id="heroShowCategories" ';
$html .= checked($showCategories, true, false) . '>';
$html .= ' <label class="form-check-label small" for="heroShowCategories">';
$html .= ' <i class="bi bi-tags me-1" style="color: #FF8600;"></i>';
$html .= ' <strong>Mostrar badges de categorías</strong>';
$html .= ' </label>';
$html .= ' </div>';
$html .= ' </div>';
$showBadgeIcon = $this->renderer->getFieldValue($componentId, 'content', 'show_badge_icon', true);
$html .= ' <div class="mb-3">';
$html .= ' <div class="form-check form-switch">';
$html .= ' <input class="form-check-input" type="checkbox" id="heroShowBadgeIcon" ';
$html .= checked($showBadgeIcon, true, false) . '>';
$html .= ' <label class="form-check-label small" for="heroShowBadgeIcon">';
$html .= ' <i class="bi bi-star me-1" style="color: #FF8600;"></i>';
$html .= ' <strong>Mostrar ícono en badges</strong>';
$html .= ' </label>';
$html .= ' </div>';
$html .= ' </div>';
$badgeIconClass = $this->renderer->getFieldValue($componentId, 'content', 'badge_icon_class', 'bi-folder-fill');
$html .= ' <div class="mb-3">';
$html .= ' <label for="heroBadgeIconClass" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-bootstrap me-1" style="color: #FF8600;"></i>';
$html .= ' Clase del ícono de badge';
$html .= ' </label>';
$html .= ' <input type="text" id="heroBadgeIconClass" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($badgeIconClass) . '" placeholder="bi-folder-fill">';
$html .= ' <small class="text-muted">Usa clases de Bootstrap Icons</small>';
$html .= ' </div>';
$titleTag = $this->renderer->getFieldValue($componentId, 'content', 'title_tag', 'h1');
$html .= ' <div class="mb-0">';
$html .= ' <label for="heroTitleTag" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-code me-1" style="color: #FF8600;"></i>';
$html .= ' Etiqueta HTML del título';
$html .= ' </label>';
$html .= ' <select id="heroTitleTag" class="form-select form-select-sm">';
$html .= ' <option value="h1" ' . selected($titleTag, 'h1', false) . '>H1 (recomendado para SEO)</option>';
$html .= ' <option value="h2" ' . selected($titleTag, 'h2', false) . '>H2</option>';
$html .= ' <option value="div" ' . selected($titleTag, 'div', false) . '>DIV (sin semántica)</option>';
$html .= ' </select>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildColorsGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
$html .= ' Colores';
$html .= ' </h5>';
$html .= ' <div class="row g-2 mb-2">';
$gradientStart = $this->renderer->getFieldValue($componentId, 'colors', 'gradient_start', '#1e3a5f');
$html .= $this->buildColorPicker('heroGradientStart', 'Degradado (inicio)', 'circle-half', $gradientStart);
$gradientEnd = $this->renderer->getFieldValue($componentId, 'colors', 'gradient_end', '#2c5282');
$html .= $this->buildColorPicker('heroGradientEnd', 'Degradado (fin)', 'circle-fill', $gradientEnd);
$titleColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_color', '#FFFFFF');
$html .= $this->buildColorPicker('heroTitleColor', 'Color título', 'fonts', $titleColor);
$badgeBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'badge_bg_color', '#FFFFFF');
$html .= $this->buildColorPicker('heroBadgeBgColor', 'Fondo badges', 'badge', $badgeBgColor);
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-0">';
$badgeTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'badge_text_color', '#FFFFFF');
$html .= $this->buildColorPicker('heroBadgeTextColor', 'Texto badges', 'card-text', $badgeTextColor);
$badgeIconColor = $this->renderer->getFieldValue($componentId, 'colors', 'badge_icon_color', '#FFB800');
$html .= $this->buildColorPicker('heroBadgeIconColor', 'Ícono badges', 'star-fill', $badgeIconColor);
$badgeHoverBg = $this->renderer->getFieldValue($componentId, 'colors', 'badge_hover_bg', '#FF8600');
$html .= $this->buildColorPicker('heroBadgeHoverBg', 'Badges (hover)', 'hand-index', $badgeHoverBg);
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildTypographyGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-type me-2" style="color: #FF8600;"></i>';
$html .= ' Tipografía';
$html .= ' </h5>';
$html .= ' <div class="row g-2 mb-2">';
$titleFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_size', '2.5rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="heroTitleFontSize" class="form-label small mb-1 fw-semibold">';
$html .= ' Tamaño desktop';
$html .= ' </label>';
$html .= ' <input type="text" id="heroTitleFontSize" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($titleFontSize) . '">';
$html .= ' </div>';
$titleFontSizeMobile = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_size_mobile', '1.75rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="heroTitleFontSizeMobile" class="form-label small mb-1 fw-semibold">';
$html .= ' Tamaño mobile';
$html .= ' </label>';
$html .= ' <input type="text" id="heroTitleFontSizeMobile" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($titleFontSizeMobile) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-2">';
$titleFontWeight = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_weight', '700');
$html .= ' <div class="col-6">';
$html .= ' <label for="heroTitleFontWeight" class="form-label small mb-1 fw-semibold">';
$html .= ' Peso del título';
$html .= ' </label>';
$html .= ' <select id="heroTitleFontWeight" class="form-select form-select-sm">';
$html .= ' <option value="400" ' . selected($titleFontWeight, '400', false) . '>Normal (400)</option>';
$html .= ' <option value="500" ' . selected($titleFontWeight, '500', false) . '>Medium (500)</option>';
$html .= ' <option value="600" ' . selected($titleFontWeight, '600', false) . '>Semibold (600)</option>';
$html .= ' <option value="700" ' . selected($titleFontWeight, '700', false) . '>Bold (700)</option>';
$html .= ' </select>';
$html .= ' </div>';
$titleLineHeight = $this->renderer->getFieldValue($componentId, 'typography', 'title_line_height', '1.4');
$html .= ' <div class="col-6">';
$html .= ' <label for="heroTitleLineHeight" class="form-label small mb-1 fw-semibold">';
$html .= ' Altura de línea';
$html .= ' </label>';
$html .= ' <input type="text" id="heroTitleLineHeight" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($titleLineHeight) . '">';
$html .= ' </div>';
$html .= ' </div>';
$badgeFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'badge_font_size', '0.813rem');
$html .= ' <div class="mb-0">';
$html .= ' <label for="heroBadgeFontSize" class="form-label small mb-1 fw-semibold">';
$html .= ' Tamaño fuente badges';
$html .= ' </label>';
$html .= ' <input type="text" id="heroBadgeFontSize" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($badgeFontSize) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildSpacingGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-arrows-move me-2" style="color: #FF8600;"></i>';
$html .= ' Espaciado';
$html .= ' </h5>';
$html .= ' <div class="row g-2 mb-2">';
$paddingVertical = $this->renderer->getFieldValue($componentId, 'spacing', 'padding_vertical', '3rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="heroPaddingVertical" class="form-label small mb-1 fw-semibold">';
$html .= ' Padding vertical';
$html .= ' </label>';
$html .= ' <input type="text" id="heroPaddingVertical" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($paddingVertical) . '">';
$html .= ' </div>';
$marginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_bottom', '1.5rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="heroMarginBottom" class="form-label small mb-1 fw-semibold">';
$html .= ' Margen inferior';
$html .= ' </label>';
$html .= ' <input type="text" id="heroMarginBottom" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($marginBottom) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-0">';
$badgePadding = $this->renderer->getFieldValue($componentId, 'spacing', 'badge_padding', '0.375rem 0.875rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="heroBadgePadding" class="form-label small mb-1 fw-semibold">';
$html .= ' Padding badges';
$html .= ' </label>';
$html .= ' <input type="text" id="heroBadgePadding" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($badgePadding) . '">';
$html .= ' </div>';
$badgeBorderRadius = $this->renderer->getFieldValue($componentId, 'spacing', 'badge_border_radius', '20px');
$html .= ' <div class="col-6">';
$html .= ' <label for="heroBadgeBorderRadius" class="form-label small mb-1 fw-semibold">';
$html .= ' Border radius badges';
$html .= ' </label>';
$html .= ' <input type="text" id="heroBadgeBorderRadius" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($badgeBorderRadius) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildEffectsGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-magic me-2" style="color: #FF8600;"></i>';
$html .= ' Efectos';
$html .= ' </h5>';
$boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', '0 4px 16px rgba(30, 58, 95, 0.25)');
$html .= ' <div class="mb-2">';
$html .= ' <label for="heroBoxShadow" class="form-label small mb-1 fw-semibold">';
$html .= ' Sombra del hero';
$html .= ' </label>';
$html .= ' <input type="text" id="heroBoxShadow" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($boxShadow) . '">';
$html .= ' </div>';
$titleTextShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'title_text_shadow', '1px 1px 2px rgba(0, 0, 0, 0.2)');
$html .= ' <div class="mb-2">';
$html .= ' <label for="heroTitleTextShadow" class="form-label small mb-1 fw-semibold">';
$html .= ' Sombra del título';
$html .= ' </label>';
$html .= ' <input type="text" id="heroTitleTextShadow" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($titleTextShadow) . '">';
$html .= ' </div>';
$badgeBackdropBlur = $this->renderer->getFieldValue($componentId, 'visual_effects', 'badge_backdrop_blur', '10px');
$html .= ' <div class="mb-0">';
$html .= ' <label for="heroBadgeBackdropBlur" class="form-label small mb-1 fw-semibold">';
$html .= ' Blur de fondo badges';
$html .= ' </label>';
$html .= ' <input type="text" id="heroBadgeBackdropBlur" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($badgeBackdropBlur) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildColorPicker(string $id, string $label, string $icon, string $value): string
{
$html = ' <div class="col-6">';
$html .= ' <label for="' . $id . '" class="form-label small mb-1 fw-semibold" style="color: #495057;">';
$html .= ' <i class="bi bi-' . $icon . ' me-1" style="color: #FF8600;"></i>';
$html .= ' ' . $label;
$html .= ' </label>';
$html .= ' <input type="color" id="' . $id . '" class="form-control form-control-color w-100" ';
$html .= ' value="' . esc_attr($value) . '" title="' . esc_attr($label) . '">';
$html .= ' <small class="text-muted d-block mt-1" id="' . $id . 'Value">' . esc_html(strtoupper($value)) . '</small>';
$html .= ' </div>';
return $html;
}
}

View File

@@ -0,0 +1,596 @@
<?php
declare(strict_types=1);
namespace ROITheme\Admin\Infrastructure\API\WordPress;
use ROITheme\Shared\Application\UseCases\SaveComponentSettings\SaveComponentSettingsUseCase;
/**
* Handler para peticiones AJAX del panel de administración
*
* Infrastructure - WordPress specific
*/
final class AdminAjaxHandler
{
public function __construct(
private readonly ?SaveComponentSettingsUseCase $saveComponentSettingsUseCase = null
) {
}
/**
* Registra los hooks de WordPress para AJAX
*/
public function register(): void
{
// Guardar configuración de componente
add_action('wp_ajax_roi_save_component_settings', [$this, 'saveComponentSettings']);
// Restaurar valores por defecto
add_action('wp_ajax_roi_reset_component_defaults', [$this, 'resetComponentDefaults']);
}
/**
* Guarda la configuración de un componente
*/
public function saveComponentSettings(): void
{
// Verificar nonce
check_ajax_referer('roi_admin_dashboard', 'nonce');
// Verificar permisos
if (!current_user_can('manage_options')) {
wp_send_json_error([
'message' => 'No tienes permisos para realizar esta acción.'
]);
}
// Obtener datos
$component = sanitize_text_field($_POST['component'] ?? '');
$settings = json_decode(stripslashes($_POST['settings'] ?? '{}'), true);
if (empty($component) || empty($settings)) {
wp_send_json_error([
'message' => 'Datos incompletos.'
]);
}
// Mapear IDs de campos a nombres de atributos del schema
$fieldMapping = $this->getFieldMapping();
$mappedSettings = [];
foreach ($settings as $fieldId => $value) {
// Convertir ID del campo a nombre del atributo
if (!isset($fieldMapping[$fieldId])) {
continue; // Campo no mapeado, ignorar
}
$mapping = $fieldMapping[$fieldId];
$groupName = $mapping['group'];
$attributeName = $mapping['attribute'];
if (!isset($mappedSettings[$groupName])) {
$mappedSettings[$groupName] = [];
}
$mappedSettings[$groupName][$attributeName] = $value;
}
// Usar Use Case para guardar
if ($this->saveComponentSettingsUseCase !== null) {
$updated = $this->saveComponentSettingsUseCase->execute($component, $mappedSettings);
wp_send_json_success([
'message' => sprintf('Se guardaron %d campos correctamente.', $updated)
]);
} else {
wp_send_json_error([
'message' => 'Error: Use Case no disponible.'
]);
}
}
/**
* Restaura los valores por defecto de un componente
*/
public function resetComponentDefaults(): void
{
// Verificar nonce
check_ajax_referer('roi_admin_dashboard', 'nonce');
// Verificar permisos
if (!current_user_can('manage_options')) {
wp_send_json_error([
'message' => 'No tienes permisos para realizar esta acción.'
]);
}
// Obtener componente
$component = sanitize_text_field($_POST['component'] ?? '');
if (empty($component)) {
wp_send_json_error([
'message' => 'Componente no especificado.'
]);
}
// Ruta al schema JSON
$schemaPath = get_template_directory() . '/Schemas/' . $component . '.json';
if (!file_exists($schemaPath)) {
wp_send_json_error([
'message' => 'Schema del componente no encontrado.'
]);
}
// Usar repositorio para restaurar valores
if ($this->saveComponentSettingsUseCase !== null) {
global $wpdb;
$repository = new \ROITheme\Shared\Infrastructure\Persistence\WordPress\WordPressComponentSettingsRepository($wpdb);
$updated = $repository->resetToDefaults($component, $schemaPath);
wp_send_json_success([
'message' => sprintf('Se restauraron %d campos a sus valores por defecto.', $updated)
]);
} else {
wp_send_json_error([
'message' => 'Error: Repositorio no disponible.'
]);
}
}
/**
* Mapeo de IDs de campos HTML a nombres de atributos del schema
*
* @return array<string, array{group: string, attribute: string}>
*/
private function getFieldMapping(): array
{
return [
// =====================================================
// TOP NOTIFICATION BAR
// =====================================================
// Activación y Visibilidad
'topBarEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
'topBarShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
'topBarShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
'topBarShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
// Contenido
'topBarIconClass' => ['group' => 'content', 'attribute' => 'icon_class'],
'topBarLabelText' => ['group' => 'content', 'attribute' => 'label_text'],
'topBarMessageText' => ['group' => 'content', 'attribute' => 'message_text'],
'topBarLinkText' => ['group' => 'content', 'attribute' => 'link_text'],
'topBarLinkUrl' => ['group' => 'content', 'attribute' => 'link_url'],
// Colores
'topBarBackgroundColor' => ['group' => 'colors', 'attribute' => 'background_color'],
'topBarTextColor' => ['group' => 'colors', 'attribute' => 'text_color'],
'topBarLabelColor' => ['group' => 'colors', 'attribute' => 'label_color'],
'topBarIconColor' => ['group' => 'colors', 'attribute' => 'icon_color'],
'topBarLinkColor' => ['group' => 'colors', 'attribute' => 'link_color'],
'topBarLinkHoverColor' => ['group' => 'colors', 'attribute' => 'link_hover_color'],
// Espaciado
'topBarFontSize' => ['group' => 'spacing', 'attribute' => 'font_size'],
'topBarPadding' => ['group' => 'spacing', 'attribute' => 'padding'],
// =====================================================
// NAVBAR
// =====================================================
// Visibility
'navbarEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
'navbarShowMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
'navbarShowDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
'navbarShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
'navbarSticky' => ['group' => 'visibility', 'attribute' => 'sticky_enabled'],
// Layout
'navbarContainerType' => ['group' => 'layout', 'attribute' => 'container_type'],
'navbarPaddingVertical' => ['group' => 'layout', 'attribute' => 'padding_vertical'],
'navbarZIndex' => ['group' => 'layout', 'attribute' => 'z_index'],
// Behavior
'navbarMenuLocation' => ['group' => 'behavior', 'attribute' => 'menu_location'],
'navbarCustomMenuId' => ['group' => 'behavior', 'attribute' => 'custom_menu_id'],
'navbarEnableDropdowns' => ['group' => 'behavior', 'attribute' => 'enable_dropdowns'],
'navbarMobileBreakpoint' => ['group' => 'behavior', 'attribute' => 'mobile_breakpoint'],
// Media (Logo/Marca)
'navbarShowBrand' => ['group' => 'media', 'attribute' => 'show_brand'],
'navbarUseLogo' => ['group' => 'media', 'attribute' => 'use_logo'],
'navbarLogoUrl' => ['group' => 'media', 'attribute' => 'logo_url'],
'navbarLogoHeight' => ['group' => 'media', 'attribute' => 'logo_height'],
'navbarBrandText' => ['group' => 'media', 'attribute' => 'brand_text'],
'navbarBrandFontSize' => ['group' => 'media', 'attribute' => 'brand_font_size'],
'navbarBrandColor' => ['group' => 'media', 'attribute' => 'brand_color'],
'navbarBrandHoverColor' => ['group' => 'media', 'attribute' => 'brand_hover_color'],
// Links
'linksTextColor' => ['group' => 'links', 'attribute' => 'text_color'],
'linksHoverColor' => ['group' => 'links', 'attribute' => 'hover_color'],
'linksActiveColor' => ['group' => 'links', 'attribute' => 'active_color'],
'linksFontSize' => ['group' => 'links', 'attribute' => 'font_size'],
'linksFontWeight' => ['group' => 'links', 'attribute' => 'font_weight'],
'linksPadding' => ['group' => 'links', 'attribute' => 'padding'],
'linksBorderRadius' => ['group' => 'links', 'attribute' => 'border_radius'],
'linksShowUnderline' => ['group' => 'links', 'attribute' => 'show_underline_effect'],
'linksUnderlineColor' => ['group' => 'links', 'attribute' => 'underline_color'],
// Visual Effects (Dropdown)
'dropdownBgColor' => ['group' => 'visual_effects', 'attribute' => 'background_color'],
'dropdownBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'border_radius'],
'dropdownShadow' => ['group' => 'visual_effects', 'attribute' => 'shadow'],
'dropdownItemColor' => ['group' => 'visual_effects', 'attribute' => 'item_color'],
'dropdownItemHoverBg' => ['group' => 'visual_effects', 'attribute' => 'item_hover_background'],
'dropdownItemPadding' => ['group' => 'visual_effects', 'attribute' => 'item_padding'],
'dropdownMaxHeight' => ['group' => 'visual_effects', 'attribute' => 'dropdown_max_height'],
// Colors (Navbar styles)
'navbarBgColor' => ['group' => 'colors', 'attribute' => 'background_color'],
'navbarBoxShadow' => ['group' => 'colors', 'attribute' => 'box_shadow'],
// =====================================================
// CTA LETS TALK
// =====================================================
// Visibility
'ctaLetsTalkEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
'ctaLetsTalkShowDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
'ctaLetsTalkShowMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
'ctaLetsTalkShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
// Content
'ctaLetsTalkButtonText' => ['group' => 'content', 'attribute' => 'button_text'],
'ctaLetsTalkShowIcon' => ['group' => 'content', 'attribute' => 'show_icon'],
'ctaLetsTalkIconClass' => ['group' => 'content', 'attribute' => 'icon_class'],
'ctaLetsTalkModalTarget' => ['group' => 'content', 'attribute' => 'modal_target'],
'ctaLetsTalkAriaLabel' => ['group' => 'content', 'attribute' => 'aria_label'],
// Behavior
'ctaLetsTalkEnableModal' => ['group' => 'behavior', 'attribute' => 'enable_modal'],
'ctaLetsTalkCustomUrl' => ['group' => 'behavior', 'attribute' => 'custom_url'],
'ctaLetsTalkOpenNewTab' => ['group' => 'behavior', 'attribute' => 'open_in_new_tab'],
// Typography
'ctaLetsTalkFontSize' => ['group' => 'typography', 'attribute' => 'font_size'],
'ctaLetsTalkFontWeight' => ['group' => 'typography', 'attribute' => 'font_weight'],
'ctaLetsTalkTextTransform' => ['group' => 'typography', 'attribute' => 'text_transform'],
// Colors
'ctaLetsTalkBgColor' => ['group' => 'colors', 'attribute' => 'background_color'],
'ctaLetsTalkBgHoverColor' => ['group' => 'colors', 'attribute' => 'background_hover_color'],
'ctaLetsTalkTextColor' => ['group' => 'colors', 'attribute' => 'text_color'],
'ctaLetsTalkTextHoverColor' => ['group' => 'colors', 'attribute' => 'text_hover_color'],
'ctaLetsTalkBorderColor' => ['group' => 'colors', 'attribute' => 'border_color'],
// Spacing
'ctaLetsTalkPaddingTB' => ['group' => 'spacing', 'attribute' => 'padding_top_bottom'],
'ctaLetsTalkPaddingLR' => ['group' => 'spacing', 'attribute' => 'padding_left_right'],
'ctaLetsTalkMarginLeft' => ['group' => 'spacing', 'attribute' => 'margin_left'],
'ctaLetsTalkIconSpacing' => ['group' => 'spacing', 'attribute' => 'icon_spacing'],
// Visual Effects
'ctaLetsTalkBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'border_radius'],
'ctaLetsTalkBorderWidth' => ['group' => 'visual_effects', 'attribute' => 'border_width'],
'ctaLetsTalkBoxShadow' => ['group' => 'visual_effects', 'attribute' => 'box_shadow'],
'ctaLetsTalkTransition' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'],
// =====================================================
// HERO SECTION
// =====================================================
// Visibility
'heroEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
'heroShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
'heroShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
'heroShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
// Content
'heroShowCategories' => ['group' => 'content', 'attribute' => 'show_categories'],
'heroShowBadgeIcon' => ['group' => 'content', 'attribute' => 'show_badge_icon'],
'heroBadgeIconClass' => ['group' => 'content', 'attribute' => 'badge_icon_class'],
'heroTitleTag' => ['group' => 'content', 'attribute' => 'title_tag'],
// Colors
'heroGradientStart' => ['group' => 'colors', 'attribute' => 'gradient_start'],
'heroGradientEnd' => ['group' => 'colors', 'attribute' => 'gradient_end'],
'heroTitleColor' => ['group' => 'colors', 'attribute' => 'title_color'],
'heroBadgeBgColor' => ['group' => 'colors', 'attribute' => 'badge_bg_color'],
'heroBadgeTextColor' => ['group' => 'colors', 'attribute' => 'badge_text_color'],
'heroBadgeIconColor' => ['group' => 'colors', 'attribute' => 'badge_icon_color'],
'heroBadgeHoverBg' => ['group' => 'colors', 'attribute' => 'badge_hover_bg'],
// Typography
'heroTitleFontSize' => ['group' => 'typography', 'attribute' => 'title_font_size'],
'heroTitleFontSizeMobile' => ['group' => 'typography', 'attribute' => 'title_font_size_mobile'],
'heroTitleFontWeight' => ['group' => 'typography', 'attribute' => 'title_font_weight'],
'heroTitleLineHeight' => ['group' => 'typography', 'attribute' => 'title_line_height'],
'heroBadgeFontSize' => ['group' => 'typography', 'attribute' => 'badge_font_size'],
// Spacing
'heroPaddingVertical' => ['group' => 'spacing', 'attribute' => 'padding_vertical'],
'heroMarginBottom' => ['group' => 'spacing', 'attribute' => 'margin_bottom'],
'heroBadgePadding' => ['group' => 'spacing', 'attribute' => 'badge_padding'],
'heroBadgeBorderRadius' => ['group' => 'spacing', 'attribute' => 'badge_border_radius'],
// Visual Effects
'heroBoxShadow' => ['group' => 'visual_effects', 'attribute' => 'box_shadow'],
'heroTitleTextShadow' => ['group' => 'visual_effects', 'attribute' => 'title_text_shadow'],
'heroBadgeBackdropBlur' => ['group' => 'visual_effects', 'attribute' => 'badge_backdrop_blur'],
// =====================================================
// FEATURED IMAGE
// =====================================================
// Visibility
'featuredImageEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
'featuredImageShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
'featuredImageShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
'featuredImageShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
// Content
'featuredImageSize' => ['group' => 'content', 'attribute' => 'image_size'],
'featuredImageLazyLoading' => ['group' => 'content', 'attribute' => 'lazy_loading'],
'featuredImageLinkToMedia' => ['group' => 'content', 'attribute' => 'link_to_media'],
// Spacing
'featuredImageMarginTop' => ['group' => 'spacing', 'attribute' => 'margin_top'],
'featuredImageMarginBottom' => ['group' => 'spacing', 'attribute' => 'margin_bottom'],
// Visual Effects
'featuredImageBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'border_radius'],
'featuredImageBoxShadow' => ['group' => 'visual_effects', 'attribute' => 'box_shadow'],
'featuredImageHoverEffect' => ['group' => 'visual_effects', 'attribute' => 'hover_effect'],
'featuredImageHoverScale' => ['group' => 'visual_effects', 'attribute' => 'hover_scale'],
'featuredImageTransitionDuration' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'],
// =====================================================
// TABLE OF CONTENTS
// =====================================================
// Visibility
'tocEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
'tocShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
'tocShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
'tocShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
// Content
'tocTitle' => ['group' => 'content', 'attribute' => 'title'],
'tocAutoGenerate' => ['group' => 'content', 'attribute' => 'auto_generate'],
'tocHeadingLevels' => ['group' => 'content', 'attribute' => 'heading_levels'],
'tocSmoothScroll' => ['group' => 'content', 'attribute' => 'smooth_scroll'],
// Typography
'tocTitleFontSize' => ['group' => 'typography', 'attribute' => 'title_font_size'],
'tocTitleFontWeight' => ['group' => 'typography', 'attribute' => 'title_font_weight'],
'tocLinkFontSize' => ['group' => 'typography', 'attribute' => 'link_font_size'],
'tocLinkLineHeight' => ['group' => 'typography', 'attribute' => 'link_line_height'],
'tocLevelThreeFontSize' => ['group' => 'typography', 'attribute' => 'level_three_font_size'],
'tocLevelFourFontSize' => ['group' => 'typography', 'attribute' => 'level_four_font_size'],
// Colors
'tocBackgroundColor' => ['group' => 'colors', 'attribute' => 'background_color'],
'tocBorderColor' => ['group' => 'colors', 'attribute' => 'border_color'],
'tocTitleColor' => ['group' => 'colors', 'attribute' => 'title_color'],
'tocTitleBorderColor' => ['group' => 'colors', 'attribute' => 'title_border_color'],
'tocLinkColor' => ['group' => 'colors', 'attribute' => 'link_color'],
'tocLinkHoverColor' => ['group' => 'colors', 'attribute' => 'link_hover_color'],
'tocLinkHoverBackground' => ['group' => 'colors', 'attribute' => 'link_hover_background'],
'tocActiveBorderColor' => ['group' => 'colors', 'attribute' => 'active_border_color'],
'tocActiveBackgroundColor' => ['group' => 'colors', 'attribute' => 'active_background_color'],
'tocActiveTextColor' => ['group' => 'colors', 'attribute' => 'active_text_color'],
'tocScrollbarTrackColor' => ['group' => 'colors', 'attribute' => 'scrollbar_track_color'],
'tocScrollbarThumbColor' => ['group' => 'colors', 'attribute' => 'scrollbar_thumb_color'],
// Spacing
'tocContainerPadding' => ['group' => 'spacing', 'attribute' => 'container_padding'],
'tocMarginBottom' => ['group' => 'spacing', 'attribute' => 'margin_bottom'],
'tocTitlePaddingBottom' => ['group' => 'spacing', 'attribute' => 'title_padding_bottom'],
'tocTitleMarginBottom' => ['group' => 'spacing', 'attribute' => 'title_margin_bottom'],
'tocItemMarginBottom' => ['group' => 'spacing', 'attribute' => 'item_margin_bottom'],
'tocLinkPadding' => ['group' => 'spacing', 'attribute' => 'link_padding'],
'tocLevelThreePaddingLeft' => ['group' => 'spacing', 'attribute' => 'level_three_padding_left'],
'tocLevelFourPaddingLeft' => ['group' => 'spacing', 'attribute' => 'level_four_padding_left'],
'tocScrollbarWidth' => ['group' => 'spacing', 'attribute' => 'scrollbar_width'],
// Visual Effects
'tocBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'border_radius'],
'tocBoxShadow' => ['group' => 'visual_effects', 'attribute' => 'box_shadow'],
'tocBorderWidth' => ['group' => 'visual_effects', 'attribute' => 'border_width'],
'tocLinkBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'link_border_radius'],
'tocActiveBorderLeftWidth' => ['group' => 'visual_effects', 'attribute' => 'active_border_left_width'],
'tocTransitionDuration' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'],
'tocScrollbarBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'scrollbar_border_radius'],
// Behavior
'tocIsSticky' => ['group' => 'behavior', 'attribute' => 'is_sticky'],
'tocScrollOffset' => ['group' => 'behavior', 'attribute' => 'scroll_offset'],
'tocMaxHeight' => ['group' => 'behavior', 'attribute' => 'max_height'],
// =====================================================
// SOCIAL SHARE
// =====================================================
// Visibility
'socialShareEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
'socialShareShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
'socialShareShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
'socialShareShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
// Content
'socialShareShowLabel' => ['group' => 'content', 'attribute' => 'show_label'],
'socialShareLabelText' => ['group' => 'content', 'attribute' => 'label_text'],
// Networks
'socialShareFacebook' => ['group' => 'networks', 'attribute' => 'show_facebook'],
'socialShareFacebookUrl' => ['group' => 'networks', 'attribute' => 'facebook_url'],
'socialShareInstagram' => ['group' => 'networks', 'attribute' => 'show_instagram'],
'socialShareInstagramUrl' => ['group' => 'networks', 'attribute' => 'instagram_url'],
'socialShareLinkedin' => ['group' => 'networks', 'attribute' => 'show_linkedin'],
'socialShareLinkedinUrl' => ['group' => 'networks', 'attribute' => 'linkedin_url'],
'socialShareWhatsapp' => ['group' => 'networks', 'attribute' => 'show_whatsapp'],
'socialShareWhatsappNumber' => ['group' => 'networks', 'attribute' => 'whatsapp_number'],
'socialShareTwitter' => ['group' => 'networks', 'attribute' => 'show_twitter'],
'socialShareTwitterUrl' => ['group' => 'networks', 'attribute' => 'twitter_url'],
'socialShareEmail' => ['group' => 'networks', 'attribute' => 'show_email'],
'socialShareEmailAddress' => ['group' => 'networks', 'attribute' => 'email_address'],
// Colors
'socialShareLabelColor' => ['group' => 'colors', 'attribute' => 'label_color'],
'socialShareBorderTopColor' => ['group' => 'colors', 'attribute' => 'border_top_color'],
'socialShareButtonBg' => ['group' => 'colors', 'attribute' => 'button_background'],
'socialShareFacebookColor' => ['group' => 'colors', 'attribute' => 'facebook_color'],
'socialShareInstagramColor' => ['group' => 'colors', 'attribute' => 'instagram_color'],
'socialShareLinkedinColor' => ['group' => 'colors', 'attribute' => 'linkedin_color'],
'socialShareWhatsappColor' => ['group' => 'colors', 'attribute' => 'whatsapp_color'],
'socialShareTwitterColor' => ['group' => 'colors', 'attribute' => 'twitter_color'],
'socialShareEmailColor' => ['group' => 'colors', 'attribute' => 'email_color'],
// Typography
'socialShareLabelFontSize' => ['group' => 'typography', 'attribute' => 'label_font_size'],
'socialShareIconFontSize' => ['group' => 'typography', 'attribute' => 'icon_font_size'],
// Spacing
'socialShareMarginTop' => ['group' => 'spacing', 'attribute' => 'container_margin_top'],
'socialShareMarginBottom' => ['group' => 'spacing', 'attribute' => 'container_margin_bottom'],
'socialSharePaddingTop' => ['group' => 'spacing', 'attribute' => 'container_padding_top'],
'socialSharePaddingBottom' => ['group' => 'spacing', 'attribute' => 'container_padding_bottom'],
'socialShareLabelMarginBottom' => ['group' => 'spacing', 'attribute' => 'label_margin_bottom'],
'socialShareButtonsGap' => ['group' => 'spacing', 'attribute' => 'buttons_gap'],
'socialShareButtonPadding' => ['group' => 'spacing', 'attribute' => 'button_padding'],
// Visual Effects
'socialShareBorderTopWidth' => ['group' => 'visual_effects', 'attribute' => 'border_top_width'],
'socialShareButtonBorderWidth' => ['group' => 'visual_effects', 'attribute' => 'button_border_width'],
'socialShareButtonBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'button_border_radius'],
'socialShareTransitionDuration' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'],
'socialShareHoverBoxShadow' => ['group' => 'visual_effects', 'attribute' => 'hover_box_shadow'],
// =====================================================
// CTA POST
// =====================================================
// Visibility
'ctaPostEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
'ctaPostShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
'ctaPostShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
'ctaPostShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
// Content
'ctaPostTitle' => ['group' => 'content', 'attribute' => 'title'],
'ctaPostDescription' => ['group' => 'content', 'attribute' => 'description'],
'ctaPostButtonText' => ['group' => 'content', 'attribute' => 'button_text'],
'ctaPostButtonUrl' => ['group' => 'content', 'attribute' => 'button_url'],
'ctaPostButtonIcon' => ['group' => 'content', 'attribute' => 'button_icon'],
// Typography
'ctaPostTitleFontSize' => ['group' => 'typography', 'attribute' => 'title_font_size'],
'ctaPostTitleFontWeight' => ['group' => 'typography', 'attribute' => 'title_font_weight'],
'ctaPostDescriptionFontSize' => ['group' => 'typography', 'attribute' => 'description_font_size'],
'ctaPostButtonFontSize' => ['group' => 'typography', 'attribute' => 'button_font_size'],
// Colors
'ctaPostGradientStart' => ['group' => 'colors', 'attribute' => 'gradient_start'],
'ctaPostGradientEnd' => ['group' => 'colors', 'attribute' => 'gradient_end'],
'ctaPostTitleColor' => ['group' => 'colors', 'attribute' => 'title_color'],
'ctaPostDescriptionColor' => ['group' => 'colors', 'attribute' => 'description_color'],
'ctaPostButtonBgColor' => ['group' => 'colors', 'attribute' => 'button_bg_color'],
'ctaPostButtonTextColor' => ['group' => 'colors', 'attribute' => 'button_text_color'],
'ctaPostButtonHoverBg' => ['group' => 'colors', 'attribute' => 'button_hover_bg'],
// Spacing
'ctaPostContainerMarginTop' => ['group' => 'spacing', 'attribute' => 'container_margin_top'],
'ctaPostContainerMarginBottom' => ['group' => 'spacing', 'attribute' => 'container_margin_bottom'],
'ctaPostContainerPadding' => ['group' => 'spacing', 'attribute' => 'container_padding'],
'ctaPostTitleMarginBottom' => ['group' => 'spacing', 'attribute' => 'title_margin_bottom'],
'ctaPostButtonIconMargin' => ['group' => 'spacing', 'attribute' => 'button_icon_margin'],
// Visual Effects
'ctaPostBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'border_radius'],
'ctaPostGradientAngle' => ['group' => 'visual_effects', 'attribute' => 'gradient_angle'],
'ctaPostButtonBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'button_border_radius'],
'ctaPostButtonPadding' => ['group' => 'visual_effects', 'attribute' => 'button_padding'],
'ctaPostTransitionDuration' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'],
'ctaPostBoxShadow' => ['group' => 'visual_effects', 'attribute' => 'box_shadow'],
// =====================================================
// CONTACT FORM
// =====================================================
// Visibility
'contactFormEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
'contactFormShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
'contactFormShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
'contactFormShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
// Content
'contactFormSectionTitle' => ['group' => 'content', 'attribute' => 'section_title'],
'contactFormSectionDescription' => ['group' => 'content', 'attribute' => 'section_description'],
'contactFormSubmitButtonText' => ['group' => 'content', 'attribute' => 'submit_button_text'],
'contactFormSubmitButtonIcon' => ['group' => 'content', 'attribute' => 'submit_button_icon'],
// Contact Info
'contactFormShowContactInfo' => ['group' => 'contact_info', 'attribute' => 'show_contact_info'],
'contactFormPhoneLabel' => ['group' => 'contact_info', 'attribute' => 'phone_label'],
'contactFormPhoneValue' => ['group' => 'contact_info', 'attribute' => 'phone_value'],
'contactFormEmailLabel' => ['group' => 'contact_info', 'attribute' => 'email_label'],
'contactFormEmailValue' => ['group' => 'contact_info', 'attribute' => 'email_value'],
'contactFormLocationLabel' => ['group' => 'contact_info', 'attribute' => 'location_label'],
'contactFormLocationValue' => ['group' => 'contact_info', 'attribute' => 'location_value'],
// Form Labels
'contactFormFullnamePlaceholder' => ['group' => 'form_labels', 'attribute' => 'fullname_placeholder'],
'contactFormCompanyPlaceholder' => ['group' => 'form_labels', 'attribute' => 'company_placeholder'],
'contactFormWhatsappPlaceholder' => ['group' => 'form_labels', 'attribute' => 'whatsapp_placeholder'],
'contactFormEmailPlaceholder' => ['group' => 'form_labels', 'attribute' => 'email_placeholder'],
'contactFormMessagePlaceholder' => ['group' => 'form_labels', 'attribute' => 'message_placeholder'],
// Integration
'contactFormWebhookUrl' => ['group' => 'integration', 'attribute' => 'webhook_url'],
'contactFormWebhookMethod' => ['group' => 'integration', 'attribute' => 'webhook_method'],
'contactFormIncludePageUrl' => ['group' => 'integration', 'attribute' => 'include_page_url'],
'contactFormIncludeTimestamp' => ['group' => 'integration', 'attribute' => 'include_timestamp'],
// Messages
'contactFormSuccessMessage' => ['group' => 'messages', 'attribute' => 'success_message'],
'contactFormErrorMessage' => ['group' => 'messages', 'attribute' => 'error_message'],
'contactFormSendingMessage' => ['group' => 'messages', 'attribute' => 'sending_message'],
'contactFormValidationRequired' => ['group' => 'messages', 'attribute' => 'validation_required'],
'contactFormValidationEmail' => ['group' => 'messages', 'attribute' => 'validation_email'],
// Colors
'contactFormSectionBgColor' => ['group' => 'colors', 'attribute' => 'section_bg_color'],
'contactFormTitleColor' => ['group' => 'colors', 'attribute' => 'title_color'],
'contactFormDescriptionColor' => ['group' => 'colors', 'attribute' => 'description_color'],
'contactFormIconColor' => ['group' => 'colors', 'attribute' => 'icon_color'],
'contactFormInfoLabelColor' => ['group' => 'colors', 'attribute' => 'info_label_color'],
'contactFormInfoValueColor' => ['group' => 'colors', 'attribute' => 'info_value_color'],
'contactFormInputBorderColor' => ['group' => 'colors', 'attribute' => 'input_border_color'],
'contactFormInputFocusBorder' => ['group' => 'colors', 'attribute' => 'input_focus_border'],
'contactFormButtonBgColor' => ['group' => 'colors', 'attribute' => 'button_bg_color'],
'contactFormButtonTextColor' => ['group' => 'colors', 'attribute' => 'button_text_color'],
'contactFormButtonHoverBg' => ['group' => 'colors', 'attribute' => 'button_hover_bg'],
'contactFormSuccessBgColor' => ['group' => 'colors', 'attribute' => 'success_bg_color'],
'contactFormSuccessTextColor' => ['group' => 'colors', 'attribute' => 'success_text_color'],
'contactFormErrorBgColor' => ['group' => 'colors', 'attribute' => 'error_bg_color'],
'contactFormErrorTextColor' => ['group' => 'colors', 'attribute' => 'error_text_color'],
// Spacing
'contactFormSectionPaddingY' => ['group' => 'spacing', 'attribute' => 'section_padding_y'],
'contactFormSectionMarginTop' => ['group' => 'spacing', 'attribute' => 'section_margin_top'],
'contactFormTitleMarginBottom' => ['group' => 'spacing', 'attribute' => 'title_margin_bottom'],
'contactFormDescriptionMarginBottom' => ['group' => 'spacing', 'attribute' => 'description_margin_bottom'],
'contactFormFormGap' => ['group' => 'spacing', 'attribute' => 'form_gap'],
// Visual Effects
'contactFormInputBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'input_border_radius'],
'contactFormButtonBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'button_border_radius'],
'contactFormButtonPadding' => ['group' => 'visual_effects', 'attribute' => 'button_padding'],
'contactFormTransitionDuration' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'],
'contactFormTextareaRows' => ['group' => 'visual_effects', 'attribute' => 'textarea_rows'],
];
}
}

View File

@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace ROITheme\Admin\Infrastructure\API\WordPress;
use ROITheme\Admin\Domain\Contracts\MenuRegistrarInterface;
use ROITheme\Admin\Domain\ValueObjects\MenuItem;
use ROITheme\Admin\Application\UseCases\RenderDashboardUseCase;
/**
* Registra el menú de administración en WordPress
*
* Infrastructure - Implementación específica de WordPress
*/
final class AdminMenuRegistrar implements MenuRegistrarInterface
{
private MenuItem $menuItem;
/**
* @param MenuItem $menuItem Configuración del menú
* @param RenderDashboardUseCase $renderUseCase Caso de uso para renderizar
*/
public function __construct(
MenuItem $menuItem,
private readonly RenderDashboardUseCase $renderUseCase
) {
$this->menuItem = $menuItem;
}
/**
* Registra el menú en WordPress
*/
public function register(): void
{
add_action('admin_menu', [$this, 'addMenuPage']);
}
/**
* Callback para agregar la página al menú de WordPress
*/
public function addMenuPage(): void
{
add_menu_page(
$this->menuItem->getPageTitle(),
$this->menuItem->getMenuTitle(),
$this->menuItem->getCapability(),
$this->menuItem->getMenuSlug(),
[$this, 'renderPage'],
$this->menuItem->getIcon(),
$this->menuItem->getPosition()
);
}
/**
* Callback para renderizar la página
*/
public function renderPage(): void
{
try {
echo $this->renderUseCase->execute('dashboard');
} catch (\Exception $e) {
echo '<div class="error"><p>Error rendering dashboard: ' . esc_html($e->getMessage()) . '</p></div>';
}
}
public function getCapability(): string
{
return $this->menuItem->getCapability();
}
public function getSlug(): string
{
return $this->menuItem->getMenuSlug();
}
}

View File

@@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace ROITheme\Admin\Infrastructure\Services;
/**
* Servicio para enqueue de assets del panel de administración
*
* Infrastructure - WordPress specific
*/
final class AdminAssetEnqueuer
{
private const ADMIN_PAGE_SLUG = 'roi-theme-admin';
public function __construct(
private readonly string $themeUri
) {
}
/**
* Registra los hooks de WordPress
*/
public function register(): void
{
add_action('admin_enqueue_scripts', [$this, 'enqueueAssets']);
}
/**
* Enqueue de assets solo en la página del dashboard
*
* @param string $hook Hook name de WordPress
*/
public function enqueueAssets(string $hook): void
{
// Solo cargar en nuestra página de admin
if (!$this->isAdminPage($hook)) {
return;
}
$this->enqueueStyles();
$this->enqueueScripts();
}
/**
* Verifica si estamos en la página del dashboard
*
* @param string $hook Hook name
* @return bool
*/
private function isAdminPage(string $hook): bool
{
return strpos($hook, self::ADMIN_PAGE_SLUG) !== false;
}
/**
* Enqueue de estilos CSS
*/
private function enqueueStyles(): void
{
// Bootstrap 5 CSS
wp_enqueue_style(
'bootstrap',
'https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css',
[],
'5.3.2'
);
// Bootstrap Icons
wp_enqueue_style(
'bootstrap-icons',
'https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css',
[],
'1.11.3'
);
// Estilos del dashboard
wp_enqueue_style(
'roi-admin-dashboard',
$this->themeUri . '/Admin/Infrastructure/Ui/Assets/Css/admin-dashboard.css',
['bootstrap', 'bootstrap-icons'],
filemtime(get_template_directory() . '/Admin/Infrastructure/Ui/Assets/Css/admin-dashboard.css')
);
}
/**
* Enqueue de scripts JavaScript
*/
private function enqueueScripts(): void
{
// Bootstrap 5 JS Bundle (incluye Popper)
// IMPORTANTE: Cargar en header (false) para que esté disponible antes del contenido
wp_enqueue_script(
'bootstrap',
'https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js',
[],
'5.3.2',
false // Load in header, not footer - required for Bootstrap tabs to work
);
// Script del dashboard
wp_enqueue_script(
'roi-admin-dashboard',
$this->themeUri . '/Admin/Infrastructure/Ui/Assets/Js/admin-dashboard.js',
['bootstrap'],
filemtime(get_template_directory() . '/Admin/Infrastructure/Ui/Assets/Js/admin-dashboard.js'),
true
);
// Pasar variables al JavaScript
wp_localize_script(
'roi-admin-dashboard',
'roiAdminDashboard',
[
'nonce' => wp_create_nonce('roi_admin_dashboard'),
'ajaxurl' => admin_url('admin-ajax.php')
]
);
}
}

View File

@@ -0,0 +1,160 @@
<?php
declare(strict_types=1);
namespace ROITheme\Admin\Infrastructure\Ui;
use ROITheme\Admin\Domain\Contracts\DashboardRendererInterface;
use ROITheme\Shared\Application\UseCases\GetComponentSettings\GetComponentSettingsUseCase;
/**
* Renderiza el dashboard del panel de administración
*
* Infrastructure - Implementación con WordPress
*/
final class AdminDashboardRenderer implements DashboardRendererInterface
{
private const SUPPORTED_VIEWS = ['dashboard'];
/**
* @param GetComponentSettingsUseCase|null $getComponentSettingsUseCase
* @param array<string, mixed> $components Componentes disponibles
*/
public function __construct(
private readonly ?GetComponentSettingsUseCase $getComponentSettingsUseCase = null,
private readonly array $components = []
) {
}
public function render(): string
{
ob_start();
require __DIR__ . '/Views/dashboard.php';
return ob_get_clean();
}
public function supports(string $viewType): bool
{
return in_array($viewType, self::SUPPORTED_VIEWS, true);
}
/**
* Obtiene los componentes disponibles
*
* @return array<string, array<string, string>>
*/
public function getComponents(): array
{
return [
'top-notification-bar' => [
'id' => 'top-notification-bar',
'label' => 'TopBar',
'icon' => 'bi-megaphone-fill',
],
'navbar' => [
'id' => 'navbar',
'label' => 'Navbar',
'icon' => 'bi-list',
],
'cta-lets-talk' => [
'id' => 'cta-lets-talk',
'label' => "Let's Talk",
'icon' => 'bi-lightning-charge-fill',
],
'hero' => [
'id' => 'hero',
'label' => 'Hero Section',
'icon' => 'bi-image',
],
'featured-image' => [
'id' => 'featured-image',
'label' => 'Featured Image',
'icon' => 'bi-card-image',
],
'table-of-contents' => [
'id' => 'table-of-contents',
'label' => 'Table of Contents',
'icon' => 'bi-list-nested',
],
'cta-box-sidebar' => [
'id' => 'cta-box-sidebar',
'label' => 'CTA Sidebar',
'icon' => 'bi-megaphone',
],
'social-share' => [
'id' => 'social-share',
'label' => 'Social Share',
'icon' => 'bi-share',
],
'cta-post' => [
'id' => 'cta-post',
'label' => 'CTA Post',
'icon' => 'bi-megaphone-fill',
],
'related-post' => [
'id' => 'related-post',
'label' => 'Related Posts',
'icon' => 'bi-grid-3x3-gap',
],
'contact-form' => [
'id' => 'contact-form',
'label' => 'Contact Form',
'icon' => 'bi-envelope-paper',
],
'footer' => [
'id' => 'footer',
'label' => 'Footer',
'icon' => 'bi-layout-text-window-reverse',
],
];
}
/**
* Obtiene las configuraciones de un componente
*
* @param string $componentName Nombre del componente
* @return array<string, array<string, mixed>> Configuraciones agrupadas por grupo
*/
public function getComponentSettings(string $componentName): array
{
if ($this->getComponentSettingsUseCase === null) {
return [];
}
return $this->getComponentSettingsUseCase->execute($componentName);
}
/**
* Obtiene el valor de un campo de configuración
*
* @param string $componentName Nombre del componente
* @param string $groupName Nombre del grupo
* @param string $attributeName Nombre del atributo
* @param mixed $default Valor por defecto si no existe
* @return mixed
*/
public function getFieldValue(string $componentName, string $groupName, string $attributeName, mixed $default = null): mixed
{
$settings = $this->getComponentSettings($componentName);
return $settings[$groupName][$attributeName] ?? $default;
}
/**
* Obtiene la clase del FormBuilder para un componente
*
* @param string $componentId ID del componente en kebab-case (ej: 'top-notification-bar')
* @return string Namespace completo del FormBuilder
*/
public function getFormBuilderClass(string $componentId): string
{
// Convertir kebab-case a PascalCase
// 'top-notification-bar' → 'TopNotificationBar'
$className = str_replace('-', '', ucwords($componentId, '-'));
// Construir namespace completo
// ROITheme\Admin\TopNotificationBar\Infrastructure\Ui\TopNotificationBarFormBuilder
return "ROITheme\\Admin\\{$className}\\Infrastructure\\Ui\\{$className}FormBuilder";
}
}

View File

@@ -0,0 +1,137 @@
/**
* Estilos para el Dashboard del Panel de Administración ROI Theme
* Siguiendo especificaciones del Design System
*/
/* Sobrescribir max-width de .card de WordPress */
.wrap.roi-admin-panel .card {
max-width: none !important;
}
/* Fix para switches de Bootstrap - resetear completamente estilos de WordPress */
.wrap.roi-admin-panel .form-switch .form-check-input {
all: unset !important;
/* Restaurar estilos necesarios de Bootstrap */
width: 2em !important;
height: 1em !important;
margin-left: -2.5em !important;
margin-right: 0.5em !important;
background-color: #dee2e6 !important;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='white'/%3e%3c/svg%3e") !important;
background-position: left center !important;
background-repeat: no-repeat !important;
background-size: contain !important;
border: 1px solid rgba(0, 0, 0, 0.25) !important;
border-radius: 2em !important;
transition: background-position 0.15s ease-in-out !important;
cursor: pointer !important;
flex-shrink: 0 !important;
appearance: none !important;
-webkit-appearance: none !important;
-moz-appearance: none !important;
}
.wrap.roi-admin-panel .form-switch .form-check-input:checked {
background-color: #0d6efd !important;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='white'/%3e%3c/svg%3e") !important;
background-position: right center !important;
border-color: #0d6efd !important;
}
.wrap.roi-admin-panel .form-switch .form-check-input::before,
.wrap.roi-admin-panel .form-switch .form-check-input::after {
display: none !important;
content: none !important;
}
.wrap.roi-admin-panel .form-switch .form-check-input:focus {
outline: 0 !important;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25) !important;
}
/* Alinear verticalmente los labels con los switches */
.wrap.roi-admin-panel .form-check {
display: flex !important;
align-items: center !important;
}
.wrap.roi-admin-panel .form-check-label {
display: inline-flex !important;
align-items: center !important;
margin-bottom: 0 !important;
padding-top: 0 !important;
}
/* Tabs Navigation */
.nav-tabs-admin {
border-bottom: 2px solid #e9ecef;
}
.nav-tabs-admin .nav-item {
margin-right: 0.1rem;
}
.nav-tabs-admin .nav-link {
color: #6c757d;
border: none;
border-bottom: 3px solid transparent;
padding: 0.3rem 0.4rem;
font-weight: 600;
font-size: 0.9rem;
transition: all 0.3s ease;
}
.nav-tabs-admin .nav-link i.bi {
margin-right: 0.2rem !important;
font-size: 0.7rem;
}
.nav-tabs-admin .nav-link:hover {
color: #FF8600;
border-bottom-color: #FFB800;
}
.nav-tabs-admin .nav-link.active {
color: #FF8600;
border-bottom-color: #FF8600;
background-color: transparent;
}
/* Tab Content */
.tab-content {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Responsive */
@media (max-width: 991px) {
.nav-tabs-admin {
flex-wrap: wrap;
}
.nav-tabs-admin .nav-link {
font-size: 0.8rem;
padding: 0.35rem 0.5rem;
}
}
@media (max-width: 767px) {
.nav-tabs-admin {
overflow-x: auto;
flex-wrap: nowrap;
}
.nav-tabs-admin .nav-item {
white-space: nowrap;
}
}

View File

@@ -0,0 +1,407 @@
/**
* JavaScript para el Dashboard del Panel de Administración ROI Theme
* Vanilla JavaScript - No frameworks
*/
(function() {
'use strict';
/**
* Inicializa el dashboard cuando el DOM está listo
*/
document.addEventListener('DOMContentLoaded', function() {
initializeTabs();
initializeFormValidation();
initializeButtons();
initializeColorPickers();
});
/**
* Inicializa el sistema de tabs
*/
function initializeTabs() {
const tabs = document.querySelectorAll('.nav-tab');
tabs.forEach(function(tab) {
tab.addEventListener('click', function(e) {
// Prevenir comportamiento por defecto si es necesario
// (En este caso dejamos que funcione la navegación normal)
});
});
}
/**
* Inicializa validación de formularios
*/
function initializeFormValidation() {
const forms = document.querySelectorAll('.roi-component-config form');
forms.forEach(function(form) {
form.addEventListener('submit', function(e) {
if (!validateForm(form)) {
e.preventDefault();
showError('Por favor, corrige los errores en el formulario.');
}
});
});
}
/**
* Valida un formulario
*
* @param {HTMLFormElement} form Formulario a validar
* @returns {boolean} True si es válido
*/
function validateForm(form) {
let isValid = true;
const requiredFields = form.querySelectorAll('[required]');
requiredFields.forEach(function(field) {
if (!field.value.trim()) {
field.classList.add('error');
isValid = false;
} else {
field.classList.remove('error');
}
});
return isValid;
}
/**
* Muestra un mensaje de error
*
* @param {string} message Mensaje a mostrar
*/
function showError(message) {
const notice = document.createElement('div');
notice.className = 'notice notice-error is-dismissible';
notice.innerHTML = '<p>' + escapeHtml(message) + '</p>';
const h1 = document.querySelector('.roi-admin-dashboard h1');
if (h1 && h1.nextElementSibling) {
h1.nextElementSibling.after(notice);
}
}
/**
* Escapa HTML para prevenir XSS
*
* @param {string} text Texto a escapar
* @returns {string} Texto escapado
*/
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Inicializa los botones del panel
*/
function initializeButtons() {
// Botón Guardar Cambios
const saveButton = document.getElementById('saveSettings');
if (saveButton) {
saveButton.addEventListener('click', handleSaveSettings);
}
// Botón Cancelar
const cancelButton = document.getElementById('cancelChanges');
if (cancelButton) {
cancelButton.addEventListener('click', handleCancelChanges);
}
// Botones Restaurar valores por defecto (dinámico para todos los componentes)
const resetButtons = document.querySelectorAll('.btn-reset-defaults[data-component]');
resetButtons.forEach(function(button) {
button.addEventListener('click', function(e) {
const componentId = this.getAttribute('data-component');
handleResetDefaults(e, componentId, this);
});
});
}
/**
* Guarda los cambios del formulario
*/
function handleSaveSettings(e) {
e.preventDefault();
// Obtener el tab activo
const activeTab = document.querySelector('.tab-pane.active');
if (!activeTab) {
showNotice('error', 'No hay ningún componente seleccionado.');
return;
}
// Obtener el ID del componente desde el tab
const componentId = activeTab.id.replace('Tab', '');
// Recopilar todos los campos del formulario activo
const formData = collectFormData(activeTab);
// Mostrar loading en el botón
const saveButton = document.getElementById('saveSettings');
const originalText = saveButton.innerHTML;
saveButton.disabled = true;
saveButton.innerHTML = '<i class="bi bi-hourglass-split me-1"></i> Guardando...';
// Enviar por AJAX
fetch(ajaxurl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
action: 'roi_save_component_settings',
nonce: roiAdminDashboard.nonce,
component: componentId,
settings: JSON.stringify(formData)
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotice('success', data.data.message || 'Cambios guardados correctamente.');
} else {
showNotice('error', data.data.message || 'Error al guardar los cambios.');
}
})
.catch(error => {
console.error('Error:', error);
showNotice('error', 'Error de conexión al guardar los cambios.');
})
.finally(() => {
saveButton.disabled = false;
saveButton.innerHTML = originalText;
});
}
/**
* Cancela los cambios y recarga la página
*/
function handleCancelChanges(e) {
e.preventDefault();
showConfirmModal(
'Cancelar cambios',
'¿Descartar todos los cambios no guardados?',
function() {
location.reload();
}
);
}
/**
* Restaura los valores por defecto de un componente
*
* @param {Event} e Evento del click
* @param {string} componentId ID del componente a resetear
* @param {HTMLElement} resetButton Elemento del botón que disparó el evento
*/
function handleResetDefaults(e, componentId, resetButton) {
e.preventDefault();
showConfirmModal(
'Restaurar valores por defecto',
'¿Restaurar todos los valores a los valores por defecto? Esta acción no se puede deshacer.',
function() {
// Mostrar loading en el botón
const originalText = resetButton.innerHTML;
resetButton.disabled = true;
resetButton.innerHTML = '<i class="bi bi-hourglass-split me-1"></i> Restaurando...';
// Enviar por AJAX
fetch(ajaxurl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
action: 'roi_reset_component_defaults',
nonce: roiAdminDashboard.nonce,
component: componentId
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotice('success', data.data.message || 'Valores restaurados correctamente.');
setTimeout(() => location.reload(), 1500);
} else {
showNotice('error', data.data.message || 'Error al restaurar los valores.');
}
})
.catch(error => {
console.error('Error:', error);
showNotice('error', 'Error de conexión al restaurar los valores.');
})
.finally(() => {
resetButton.disabled = false;
resetButton.innerHTML = originalText;
});
}
);
}
/**
* Recopila los datos del formulario del tab activo
*/
function collectFormData(container) {
const formData = {};
// Inputs de texto, textarea, select, color, number, email, password
const textInputs = container.querySelectorAll('input[type="text"], input[type="url"], input[type="color"], input[type="number"], input[type="email"], input[type="password"], textarea, select');
textInputs.forEach(input => {
if (input.id) {
formData[input.id] = input.value;
}
});
// Checkboxes (switches)
const checkboxes = container.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach(checkbox => {
if (checkbox.id) {
formData[checkbox.id] = checkbox.checked;
}
});
return formData;
}
/**
* Muestra un toast de Bootstrap
*/
function showNotice(type, message) {
// Mapear tipos
const typeMap = {
'success': { bg: 'success', icon: 'bi-check-circle-fill', text: 'Éxito' },
'error': { bg: 'danger', icon: 'bi-x-circle-fill', text: 'Error' },
'warning': { bg: 'warning', icon: 'bi-exclamation-triangle-fill', text: 'Advertencia' },
'info': { bg: 'info', icon: 'bi-info-circle-fill', text: 'Información' }
};
const config = typeMap[type] || typeMap['info'];
// Crear container de toasts si no existe
let toastContainer = document.getElementById('roiToastContainer');
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.id = 'roiToastContainer';
toastContainer.className = 'toast-container position-fixed start-50 translate-middle-x';
toastContainer.style.top = '60px';
toastContainer.style.zIndex = '999999';
document.body.appendChild(toastContainer);
}
// Crear toast
const toastId = 'toast-' + Date.now();
const toastHTML = `
<div id="${toastId}" class="toast align-items-center text-white bg-${config.bg} border-0" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">
<i class="bi ${config.icon} me-2"></i>
<strong>${escapeHtml(message)}</strong>
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
`;
toastContainer.insertAdjacentHTML('beforeend', toastHTML);
// Mostrar toast
const toastElement = document.getElementById(toastId);
const toast = new bootstrap.Toast(toastElement, {
autohide: true,
delay: 5000
});
toast.show();
// Eliminar del DOM después de ocultarse
toastElement.addEventListener('hidden.bs.toast', function() {
toastElement.remove();
});
}
/**
* Muestra un modal de confirmación de Bootstrap
*/
function showConfirmModal(title, message, onConfirm) {
// Crear modal si no existe
let modal = document.getElementById('roiConfirmModal');
if (!modal) {
const modalHTML = `
<div class="modal fade" id="roiConfirmModal" tabindex="-1" aria-labelledby="roiConfirmModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header" style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-bottom: none;">
<h5 class="modal-title text-white" id="roiConfirmModalLabel">
<i class="bi bi-question-circle me-2" style="color: #FF8600;"></i>
<span id="roiConfirmModalTitle">Confirmar</span>
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="roiConfirmModalBody" style="padding: 2rem;">
Mensaje de confirmación
</div>
<div class="modal-footer" style="border-top: 1px solid #dee2e6; padding: 1rem 1.5rem;">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
<i class="bi bi-x-circle me-1"></i>
Cancelar
</button>
<button type="button" class="btn text-white" id="roiConfirmModalConfirm" style="background-color: #FF8600; border-color: #FF8600;">
<i class="bi bi-check-circle me-1"></i>
Confirmar
</button>
</div>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHTML);
modal = document.getElementById('roiConfirmModal');
}
// Actualizar contenido
document.getElementById('roiConfirmModalTitle').textContent = title;
document.getElementById('roiConfirmModalBody').textContent = message;
// Configurar callback
const confirmButton = document.getElementById('roiConfirmModalConfirm');
const newConfirmButton = confirmButton.cloneNode(true);
confirmButton.parentNode.replaceChild(newConfirmButton, confirmButton);
newConfirmButton.addEventListener('click', function() {
const bsModal = bootstrap.Modal.getInstance(modal);
bsModal.hide();
if (typeof onConfirm === 'function') {
onConfirm();
}
});
// Mostrar modal
const bsModal = new bootstrap.Modal(modal);
bsModal.show();
}
/**
* Inicializa los color pickers para mostrar el valor HEX
*/
function initializeColorPickers() {
const colorPickers = document.querySelectorAll('input[type="color"]');
colorPickers.forEach(picker => {
// Elemento donde se muestra el valor HEX
const valueDisplay = document.getElementById(picker.id + 'Value');
if (valueDisplay) {
picker.addEventListener('input', function() {
valueDisplay.textContent = this.value.toUpperCase();
});
}
});
}
})();

View File

@@ -0,0 +1,76 @@
<?php
/**
* ROI Theme - Panel de Administración Principal
*
* @var AdminDashboardRenderer $this
*/
declare(strict_types=1);
// Prevenir acceso directo
if (!defined('ABSPATH')) {
exit;
}
$components = $this->getComponents();
$firstComponentId = array_key_first($components);
?>
<div class="wrap roi-admin-panel">
<!-- Navigation Tabs -->
<ul class="nav nav-tabs nav-tabs-admin mb-0" role="tablist">
<?php foreach ($components as $componentId => $component): ?>
<li class="nav-item" role="presentation">
<button class="nav-link <?php echo $componentId === $firstComponentId ? 'active' : ''; ?>"
data-bs-toggle="tab"
data-bs-target="#<?php echo esc_attr($componentId); ?>Tab"
type="button"
role="tab"
aria-controls="<?php echo esc_attr($componentId); ?>Tab"
aria-selected="<?php echo $componentId === $firstComponentId ? 'true' : 'false'; ?>">
<i class="bi <?php echo esc_attr($component['icon']); ?> me-1"></i>
<?php echo esc_html($component['label']); ?>
</button>
</li>
<?php endforeach; ?>
</ul>
<!-- Tab Content -->
<div class="tab-content mt-3">
<?php foreach ($components as $componentId => $component):
$isFirst = ($componentId === $firstComponentId);
$componentSettings = $this->getComponentSettings($componentId);
?>
<!-- Tab: <?php echo esc_html($component['label']); ?> -->
<div class="tab-pane fade <?php echo $isFirst ? 'show active' : ''; ?>"
id="<?php echo esc_attr($componentId); ?>Tab"
role="tabpanel">
<?php
// Renderizar FormBuilder del componente
$formBuilderClass = $this->getFormBuilderClass($componentId);
if (class_exists($formBuilderClass)) {
$formBuilder = new $formBuilderClass($this);
echo $formBuilder->buildForm($componentId);
} else {
echo '<p class="text-danger">FormBuilder no encontrado: ' . esc_html($formBuilderClass) . '</p>';
}
?>
</div>
<?php endforeach; ?>
</div>
<!-- Botones Globales Save/Cancel -->
<div class="d-flex justify-content-end gap-2 p-3 rounded border mt-4" style="background-color: #f8f9fa; border-color: #e9ecef !important;">
<button type="button" class="btn btn-outline-secondary" id="cancelChanges">
<i class="bi bi-x-circle me-1"></i>
Cancelar
</button>
<button type="button" id="saveSettings" class="btn fw-semibold text-white" style="background-color: #FF8600; border-color: #FF8600;">
<i class="bi bi-check-circle me-1"></i>
Guardar Cambios
</button>
</div>
</div><!-- /wrap -->

View File

@@ -0,0 +1,517 @@
<?php
declare(strict_types=1);
namespace ROITheme\Admin\Navbar\Infrastructure\Ui;
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
final class NavbarFormBuilder
{
public function __construct(
private AdminDashboardRenderer $renderer
) {}
public function buildForm(string $componentId): string
{
$html = '';
// Header
$html .= $this->buildHeader($componentId);
// Layout 2 columnas
$html .= '<div class="row g-3">';
$html .= ' <div class="col-lg-6">';
$html .= $this->buildVisibilityGroup($componentId);
$html .= $this->buildLayoutGroup($componentId);
$html .= $this->buildBehaviorGroup($componentId);
$html .= $this->buildMediaGroup($componentId);
$html .= ' </div>';
$html .= ' <div class="col-lg-6">';
$html .= $this->buildLinksGroup($componentId);
$html .= $this->buildVisualEffectsGroup($componentId);
$html .= $this->buildColorsGroup($componentId);
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildHeader(string $componentId): string
{
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
$html .= ' <div>';
$html .= ' <h3 class="h4 mb-1 fw-bold">';
$html .= ' <i class="bi bi-menu-button-wide me-2" style="color: #FF8600;"></i>';
$html .= ' Configuración de Navbar';
$html .= ' </h3>';
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
$html .= ' Personaliza el menú de navegación principal del sitio';
$html .= ' </p>';
$html .= ' </div>';
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="navbar">';
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
$html .= ' Restaurar valores por defecto';
$html .= ' </button>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildVisibilityGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
$html .= ' Activación y Visibilidad';
$html .= ' </h5>';
// Switch: Enabled
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
$html .= ' <div class="mb-2">';
$html .= ' <div class="form-check form-switch">';
$html .= ' <input class="form-check-input" type="checkbox" id="navbarEnabled" name="visibility[is_enabled]" ';
$html .= checked($enabled, true, false) . '>';
$html .= ' <label class="form-check-label small" for="navbarEnabled">';
$html .= ' <strong>Activar Navbar</strong>';
$html .= ' </label>';
$html .= ' </div>';
$html .= ' </div>';
// Switch: Show on Mobile
$showMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
$html .= ' <div class="mb-2">';
$html .= ' <div class="form-check form-switch">';
$html .= ' <input class="form-check-input" type="checkbox" id="navbarShowMobile" name="visibility[show_on_mobile]" ';
$html .= checked($showMobile, true, false) . '>';
$html .= ' <label class="form-check-label small" for="navbarShowMobile">';
$html .= ' <strong>Mostrar en Mobile</strong>';
$html .= ' </label>';
$html .= ' </div>';
$html .= ' </div>';
// Switch: Show on Desktop
$showDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
$html .= ' <div class="mb-2">';
$html .= ' <div class="form-check form-switch">';
$html .= ' <input class="form-check-input" type="checkbox" id="navbarShowDesktop" name="visibility[show_on_desktop]" ';
$html .= checked($showDesktop, true, false) . '>';
$html .= ' <label class="form-check-label small" for="navbarShowDesktop">';
$html .= ' <strong>Mostrar en Desktop</strong>';
$html .= ' </label>';
$html .= ' </div>';
$html .= ' </div>';
// Select: Show on Pages
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all');
$html .= ' <div class="mb-2">';
$html .= ' <label for="navbarShowOnPages" class="form-label small mb-1 fw-semibold">Mostrar en</label>';
$html .= ' <select id="navbarShowOnPages" name="visibility[show_on_pages]" class="form-select form-select-sm">';
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las páginas</option>';
$html .= ' <option value="home" ' . selected($showOnPages, 'home', false) . '>Solo página de inicio</option>';
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo páginas</option>';
$html .= ' </select>';
$html .= ' </div>';
// Switch: Sticky
$sticky = $this->renderer->getFieldValue($componentId, 'visibility', 'sticky_enabled', true);
$html .= ' <div class="mb-0">';
$html .= ' <div class="form-check form-switch">';
$html .= ' <input class="form-check-input" type="checkbox" id="navbarSticky" name="visibility[sticky_enabled]" ';
$html .= checked($sticky, true, false) . '>';
$html .= ' <label class="form-check-label small" for="navbarSticky">';
$html .= ' <strong>Navbar fijo (sticky)</strong>';
$html .= ' </label>';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildLayoutGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-layout-sidebar me-2" style="color: #FF8600;"></i>';
$html .= ' Layout y Estructura';
$html .= ' </h5>';
// Container Type
$containerType = $this->renderer->getFieldValue($componentId, 'layout', 'container_type', 'container');
$html .= ' <div class="mb-2">';
$html .= ' <label for="navbarContainerType" class="form-label small mb-1 fw-semibold">Tipo de contenedor</label>';
$html .= ' <select id="navbarContainerType" name="layout[container_type]" class="form-select form-select-sm">';
$html .= ' <option value="container" ' . selected($containerType, 'container', false) . '>Container (ancho fijo)</option>';
$html .= ' <option value="container-fluid" ' . selected($containerType, 'container-fluid', false) . '>Container Fluid (ancho completo)</option>';
$html .= ' </select>';
$html .= ' </div>';
// Padding Vertical
$paddingVertical = $this->renderer->getFieldValue($componentId, 'layout', 'padding_vertical', '0.75rem 0');
$html .= ' <div class="mb-2">';
$html .= ' <label for="navbarPaddingVertical" class="form-label small mb-1 fw-semibold">Padding vertical</label>';
$html .= ' <input type="text" id="navbarPaddingVertical" name="layout[padding_vertical]" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($paddingVertical) . '" placeholder="0.75rem 0">';
$html .= ' </div>';
// Z-index
$zIndex = $this->renderer->getFieldValue($componentId, 'layout', 'z_index', '1030');
$html .= ' <div class="mb-0">';
$html .= ' <label for="navbarZIndex" class="form-label small mb-1 fw-semibold">Z-index</label>';
$html .= ' <input type="text" id="navbarZIndex" name="layout[z_index]" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($zIndex) . '" placeholder="1030">';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildBehaviorGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-list me-2" style="color: #FF8600;"></i>';
$html .= ' Configuración del Menú';
$html .= ' </h5>';
// Menu Location
$menuLocation = $this->renderer->getFieldValue($componentId, 'behavior', 'menu_location', 'primary');
$html .= ' <div class="mb-2">';
$html .= ' <label for="navbarMenuLocation" class="form-label small mb-1 fw-semibold">Ubicación del menú</label>';
$html .= ' <select id="navbarMenuLocation" name="behavior[menu_location]" class="form-select form-select-sm">';
$html .= ' <option value="primary" ' . selected($menuLocation, 'primary', false) . '>Menú Principal</option>';
$html .= ' <option value="secondary" ' . selected($menuLocation, 'secondary', false) . '>Menú Secundario</option>';
$html .= ' <option value="custom" ' . selected($menuLocation, 'custom', false) . '>Menú personalizado</option>';
$html .= ' </select>';
$html .= ' </div>';
// Custom Menu ID
$customMenuId = $this->renderer->getFieldValue($componentId, 'behavior', 'custom_menu_id', '0');
$html .= ' <div class="mb-2">';
$html .= ' <label for="navbarCustomMenuId" class="form-label small mb-1 fw-semibold">ID del menú personalizado</label>';
$html .= ' <input type="text" id="navbarCustomMenuId" name="behavior[custom_menu_id]" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($customMenuId) . '" placeholder="0">';
$html .= ' </div>';
// Enable Dropdowns
$enableDropdowns = $this->renderer->getFieldValue($componentId, 'behavior', 'enable_dropdowns', true);
$html .= ' <div class="mb-2">';
$html .= ' <div class="form-check form-switch">';
$html .= ' <input class="form-check-input" type="checkbox" id="navbarEnableDropdowns" name="behavior[enable_dropdowns]" ';
$html .= checked($enableDropdowns, true, false) . '>';
$html .= ' <label class="form-check-label small" for="navbarEnableDropdowns">';
$html .= ' <strong>Habilitar submenús desplegables</strong>';
$html .= ' </label>';
$html .= ' </div>';
$html .= ' </div>';
// Mobile Breakpoint
$mobileBreakpoint = $this->renderer->getFieldValue($componentId, 'behavior', 'mobile_breakpoint', 'lg');
$html .= ' <div class="mb-0">';
$html .= ' <label for="navbarMobileBreakpoint" class="form-label small mb-1 fw-semibold">Breakpoint para menú móvil</label>';
$html .= ' <select id="navbarMobileBreakpoint" name="behavior[mobile_breakpoint]" class="form-select form-select-sm">';
$html .= ' <option value="sm" ' . selected($mobileBreakpoint, 'sm', false) . '>Small (576px)</option>';
$html .= ' <option value="md" ' . selected($mobileBreakpoint, 'md', false) . '>Medium (768px)</option>';
$html .= ' <option value="lg" ' . selected($mobileBreakpoint, 'lg', false) . '>Large (992px)</option>';
$html .= ' <option value="xl" ' . selected($mobileBreakpoint, 'xl', false) . '>Extra Large (1200px)</option>';
$html .= ' </select>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildMediaGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-image me-2" style="color: #FF8600;"></i>';
$html .= ' Logo/Marca';
$html .= ' </h5>';
// Show Brand
$showBrand = $this->renderer->getFieldValue($componentId, 'media', 'show_brand', false);
$html .= ' <div class="mb-2">';
$html .= ' <div class="form-check form-switch">';
$html .= ' <input class="form-check-input" type="checkbox" id="navbarShowBrand" name="media[show_brand]" ';
$html .= checked($showBrand, true, false) . '>';
$html .= ' <label class="form-check-label small" for="navbarShowBrand">';
$html .= ' <strong>Mostrar logo/marca</strong>';
$html .= ' </label>';
$html .= ' </div>';
$html .= ' </div>';
// Use Logo
$useLogo = $this->renderer->getFieldValue($componentId, 'media', 'use_logo', false);
$html .= ' <div class="mb-2">';
$html .= ' <div class="form-check form-switch">';
$html .= ' <input class="form-check-input" type="checkbox" id="navbarUseLogo" name="media[use_logo]" ';
$html .= checked($useLogo, true, false) . '>';
$html .= ' <label class="form-check-label small" for="navbarUseLogo">';
$html .= ' <strong>Usar logo (imagen)</strong>';
$html .= ' </label>';
$html .= ' </div>';
$html .= ' </div>';
// Logo URL
$logoUrl = $this->renderer->getFieldValue($componentId, 'media', 'logo_url', '');
$html .= ' <div class="mb-2">';
$html .= ' <label for="navbarLogoUrl" class="form-label small mb-1 fw-semibold">URL del logo</label>';
$html .= ' <input type="text" id="navbarLogoUrl" name="media[logo_url]" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($logoUrl) . '" placeholder="https://...">';
$html .= ' </div>';
// Logo Height
$logoHeight = $this->renderer->getFieldValue($componentId, 'media', 'logo_height', '40px');
$html .= ' <div class="mb-2">';
$html .= ' <label for="navbarLogoHeight" class="form-label small mb-1 fw-semibold">Altura del logo</label>';
$html .= ' <input type="text" id="navbarLogoHeight" name="media[logo_height]" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($logoHeight) . '" placeholder="40px">';
$html .= ' </div>';
// Brand Text
$brandText = $this->renderer->getFieldValue($componentId, 'media', 'brand_text', 'Mi Sitio');
$html .= ' <div class="mb-2">';
$html .= ' <label for="navbarBrandText" class="form-label small mb-1 fw-semibold">Texto de la marca</label>';
$html .= ' <input type="text" id="navbarBrandText" name="media[brand_text]" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($brandText) . '" maxlength="50">';
$html .= ' </div>';
// Brand Font Size
$brandFontSize = $this->renderer->getFieldValue($componentId, 'media', 'brand_font_size', '1.5rem');
$html .= ' <div class="mb-2">';
$html .= ' <label for="navbarBrandFontSize" class="form-label small mb-1 fw-semibold">Tamaño de fuente</label>';
$html .= ' <input type="text" id="navbarBrandFontSize" name="media[brand_font_size]" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($brandFontSize) . '" placeholder="1.5rem">';
$html .= ' </div>';
// Brand Color
$brandColor = $this->renderer->getFieldValue($componentId, 'media', 'brand_color', '#FFFFFF');
$html .= ' <div class="mb-2">';
$html .= ' <label for="navbarBrandColor" class="form-label small mb-1 fw-semibold">Color de la marca</label>';
$html .= ' <input type="color" id="navbarBrandColor" name="media[brand_color]" class="form-control form-control-color w-100" ';
$html .= ' value="' . esc_attr($brandColor) . '">';
$html .= ' </div>';
// Brand Hover Color
$brandHoverColor = $this->renderer->getFieldValue($componentId, 'media', 'brand_hover_color', '#FF8600');
$html .= ' <div class="mb-0">';
$html .= ' <label for="navbarBrandHoverColor" class="form-label small mb-1 fw-semibold">Color hover de la marca</label>';
$html .= ' <input type="color" id="navbarBrandHoverColor" name="media[brand_hover_color]" class="form-control form-control-color w-100" ';
$html .= ' value="' . esc_attr($brandHoverColor) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildLinksGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-link-45deg me-2" style="color: #FF8600;"></i>';
$html .= ' Estilos de Enlaces';
$html .= ' </h5>';
// Text Color
$textColor = $this->renderer->getFieldValue($componentId, 'links', 'text_color', '#FFFFFF');
$html .= ' <div class="mb-2">';
$html .= ' <label for="linksTextColor" class="form-label small mb-1 fw-semibold">Color del texto</label>';
$html .= ' <input type="color" id="linksTextColor" name="links[text_color]" class="form-control form-control-color w-100" ';
$html .= ' value="' . esc_attr($textColor) . '">';
$html .= ' </div>';
// Hover Color
$hoverColor = $this->renderer->getFieldValue($componentId, 'links', 'hover_color', '#FF8600');
$html .= ' <div class="mb-2">';
$html .= ' <label for="linksHoverColor" class="form-label small mb-1 fw-semibold">Color hover</label>';
$html .= ' <input type="color" id="linksHoverColor" name="links[hover_color]" class="form-control form-control-color w-100" ';
$html .= ' value="' . esc_attr($hoverColor) . '">';
$html .= ' </div>';
// Active Color
$activeColor = $this->renderer->getFieldValue($componentId, 'links', 'active_color', '#FF8600');
$html .= ' <div class="mb-2">';
$html .= ' <label for="linksActiveColor" class="form-label small mb-1 fw-semibold">Color del item activo</label>';
$html .= ' <input type="color" id="linksActiveColor" name="links[active_color]" class="form-control form-control-color w-100" ';
$html .= ' value="' . esc_attr($activeColor) . '">';
$html .= ' </div>';
// Font Size
$fontSize = $this->renderer->getFieldValue($componentId, 'links', 'font_size', '0.9rem');
$html .= ' <div class="mb-2">';
$html .= ' <label for="linksFontSize" class="form-label small mb-1 fw-semibold">Tamaño de fuente</label>';
$html .= ' <input type="text" id="linksFontSize" name="links[font_size]" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($fontSize) . '" placeholder="0.9rem">';
$html .= ' </div>';
// Font Weight
$fontWeight = $this->renderer->getFieldValue($componentId, 'links', 'font_weight', '500');
$html .= ' <div class="mb-2">';
$html .= ' <label for="linksFontWeight" class="form-label small mb-1 fw-semibold">Grosor de fuente</label>';
$html .= ' <input type="text" id="linksFontWeight" name="links[font_weight]" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($fontWeight) . '" placeholder="500">';
$html .= ' </div>';
// Padding
$padding = $this->renderer->getFieldValue($componentId, 'links', 'padding', '0.5rem 0.65rem');
$html .= ' <div class="mb-2">';
$html .= ' <label for="linksPadding" class="form-label small mb-1 fw-semibold">Padding de enlaces</label>';
$html .= ' <input type="text" id="linksPadding" name="links[padding]" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($padding) . '" placeholder="0.5rem 0.65rem">';
$html .= ' </div>';
// Border Radius
$borderRadius = $this->renderer->getFieldValue($componentId, 'links', 'border_radius', '4px');
$html .= ' <div class="mb-2">';
$html .= ' <label for="linksBorderRadius" class="form-label small mb-1 fw-semibold">Border radius hover</label>';
$html .= ' <input type="text" id="linksBorderRadius" name="links[border_radius]" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($borderRadius) . '" placeholder="4px">';
$html .= ' </div>';
// Show Underline Effect
$showUnderline = $this->renderer->getFieldValue($componentId, 'links', 'show_underline_effect', true);
$html .= ' <div class="mb-2">';
$html .= ' <div class="form-check form-switch">';
$html .= ' <input class="form-check-input" type="checkbox" id="linksShowUnderline" name="links[show_underline_effect]" ';
$html .= checked($showUnderline, true, false) . '>';
$html .= ' <label class="form-check-label small" for="linksShowUnderline">';
$html .= ' <strong>Mostrar efecto de subrayado</strong>';
$html .= ' </label>';
$html .= ' </div>';
$html .= ' </div>';
// Underline Color
$underlineColor = $this->renderer->getFieldValue($componentId, 'links', 'underline_color', '#FF8600');
$html .= ' <div class="mb-0">';
$html .= ' <label for="linksUnderlineColor" class="form-label small mb-1 fw-semibold">Color del subrayado</label>';
$html .= ' <input type="color" id="linksUnderlineColor" name="links[underline_color]" class="form-control form-control-color w-100" ';
$html .= ' value="' . esc_attr($underlineColor) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildVisualEffectsGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-chevron-down me-2" style="color: #FF8600;"></i>';
$html .= ' Estilos de Dropdown';
$html .= ' </h5>';
// Background Color
$bgColor = $this->renderer->getFieldValue($componentId, 'visual_effects', 'background_color', '#FFFFFF');
$html .= ' <div class="mb-2">';
$html .= ' <label for="dropdownBgColor" class="form-label small mb-1 fw-semibold">Fondo de dropdown</label>';
$html .= ' <input type="color" id="dropdownBgColor" name="visual_effects[background_color]" class="form-control form-control-color w-100" ';
$html .= ' value="' . esc_attr($bgColor) . '">';
$html .= ' </div>';
// Border Radius
$borderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_radius', '8px');
$html .= ' <div class="mb-2">';
$html .= ' <label for="dropdownBorderRadius" class="form-label small mb-1 fw-semibold">Border radius</label>';
$html .= ' <input type="text" id="dropdownBorderRadius" name="visual_effects[border_radius]" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($borderRadius) . '" placeholder="8px">';
$html .= ' </div>';
// Shadow
$shadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'shadow', '0 8px 24px rgba(0, 0, 0, 0.12)');
$html .= ' <div class="mb-2">';
$html .= ' <label for="dropdownShadow" class="form-label small mb-1 fw-semibold">Sombra del dropdown</label>';
$html .= ' <input type="text" id="dropdownShadow" name="visual_effects[shadow]" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($shadow) . '">';
$html .= ' </div>';
// Item Color
$itemColor = $this->renderer->getFieldValue($componentId, 'visual_effects', 'item_color', '#495057');
$html .= ' <div class="mb-2">';
$html .= ' <label for="dropdownItemColor" class="form-label small mb-1 fw-semibold">Color de items</label>';
$html .= ' <input type="color" id="dropdownItemColor" name="visual_effects[item_color]" class="form-control form-control-color w-100" ';
$html .= ' value="' . esc_attr($itemColor) . '">';
$html .= ' </div>';
// Item Hover Background
$itemHoverBg = $this->renderer->getFieldValue($componentId, 'visual_effects', 'item_hover_background', 'rgba(255, 133, 0, 0.1)');
$html .= ' <div class="mb-2">';
$html .= ' <label for="dropdownItemHoverBg" class="form-label small mb-1 fw-semibold">Fondo hover de items</label>';
$html .= ' <input type="text" id="dropdownItemHoverBg" name="visual_effects[item_hover_background]" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($itemHoverBg) . '">';
$html .= ' </div>';
// Item Padding
$itemPadding = $this->renderer->getFieldValue($componentId, 'visual_effects', 'item_padding', '0.625rem 1.25rem');
$html .= ' <div class="mb-2">';
$html .= ' <label for="dropdownItemPadding" class="form-label small mb-1 fw-semibold">Padding de items</label>';
$html .= ' <input type="text" id="dropdownItemPadding" name="visual_effects[item_padding]" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($itemPadding) . '" placeholder="0.625rem 1.25rem">';
$html .= ' </div>';
// Dropdown Max Height
$dropdownMaxHeight = $this->renderer->getFieldValue($componentId, 'visual_effects', 'dropdown_max_height', '300px');
$html .= ' <div class="mb-0">';
$html .= ' <label for="dropdownMaxHeight" class="form-label small mb-1 fw-semibold">Altura máxima del dropdown</label>';
$html .= ' <input type="text" id="dropdownMaxHeight" name="visual_effects[dropdown_max_height]" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($dropdownMaxHeight) . '" placeholder="300px">';
$html .= ' <small class="text-muted">Si se excede, aparece scroll vertical</small>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildColorsGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
$html .= ' Estilos del Navbar';
$html .= ' </h5>';
// Background Color
$navbarBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'background_color', '#1e3a5f');
$html .= ' <div class="mb-2">';
$html .= ' <label for="navbarBgColor" class="form-label small mb-1 fw-semibold">Color de fondo</label>';
$html .= ' <input type="color" id="navbarBgColor" name="colors[background_color]" class="form-control form-control-color w-100" ';
$html .= ' value="' . esc_attr($navbarBgColor) . '">';
$html .= ' </div>';
// Box Shadow
$boxShadow = $this->renderer->getFieldValue($componentId, 'colors', 'box_shadow', '0 4px 12px rgba(30, 58, 95, 0.15)');
$html .= ' <div class="mb-0">';
$html .= ' <label for="navbarBoxShadow" class="form-label small mb-1 fw-semibold">Sombra del navbar</label>';
$html .= ' <input type="text" id="navbarBoxShadow" name="colors[box_shadow]" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($boxShadow) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
}

View File

@@ -0,0 +1,544 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Navbar - Preview de Diseño</title>
<!-- Bootstrap 5 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Poppins', sans-serif;
background-color: #f0f0f1;
padding: 20px;
}
</style>
</head>
<body>
<!-- ============================================================
TAB: NAVBAR CONFIGURATION
============================================================ -->
<div class="tab-pane fade show active" id="navbarTab" role="tabpanel">
<!-- ========================================
PATRÓN 1: HEADER CON GRADIENTE
======================================== -->
<div class="rounded p-4 mb-4 shadow text-white" style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
<div>
<h3 class="h4 mb-1 fw-bold">
<i class="bi bi-list-ul me-2" style="color: #FF8600;"></i>
Configuración de Navbar
</h3>
<p class="mb-0 small" style="opacity: 0.85;">
Personaliza el menú de navegación principal del sitio
</p>
</div>
<button type="button" class="btn btn-sm btn-outline-light" id="resetNavbarDefaults">
<i class="bi bi-arrow-counterclockwise me-1"></i>
Restaurar valores por defecto
</button>
</div>
</div>
<!-- ========================================
PATRÓN 2: LAYOUT 2 COLUMNAS
======================================== -->
<div class="row g-3">
<div class="col-lg-6">
<!-- ========================================
GRUPO 1: ACTIVACIÓN Y VISIBILIDAD (OBLIGATORIO)
PATRÓN 3: CARD CON BORDER-LEFT NAVY
======================================== -->
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
<div class="card-body">
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
<i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>
Activación y Visibilidad
</h5>
<!-- ⚠️ PATRÓN 4: SWITCHES VERTICALES CON ICONOS (3 OBLIGATORIOS) -->
<!-- Switch 1: Enabled (OBLIGATORIO) -->
<div class="mb-2">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="navbarEnabled" checked>
<label class="form-check-label small" for="navbarEnabled" style="color: #495057;">
<i class="bi bi-power me-1" style="color: #FF8600;"></i>
<strong>Activar Navbar</strong>
</label>
</div>
</div>
<!-- Switch 2: Show on Mobile (OBLIGATORIO) -->
<div class="mb-2">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="navbarShowOnMobile" checked>
<label class="form-check-label small" for="navbarShowOnMobile" style="color: #495057;">
<i class="bi bi-phone me-1" style="color: #FF8600;"></i>
<strong>Mostrar en Mobile</strong> <span class="text-muted">(&lt;768px)</span>
</label>
</div>
</div>
<!-- Switch 3: Show on Desktop (OBLIGATORIO) -->
<div class="mb-2">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="navbarShowOnDesktop" checked>
<label class="form-check-label small" for="navbarShowOnDesktop" style="color: #495057;">
<i class="bi bi-display me-1" style="color: #FF8600;"></i>
<strong>Mostrar en Desktop</strong> <span class="text-muted">(≥768px)</span>
</label>
</div>
</div>
<!-- Campo adicional del schema: show_on_pages (select) -->
<div class="mb-2 mt-3">
<label for="navbarShowOnPages" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>
Mostrar en
</label>
<select id="navbarShowOnPages" class="form-select form-select-sm">
<option value="all" selected>Todas las páginas</option>
<option value="home">Solo página de inicio</option>
<option value="posts">Solo posts individuales</option>
<option value="pages">Solo páginas</option>
</select>
</div>
<!-- Switch 5: Sticky Enabled -->
<div class="mb-0 mt-2">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="navbarStickyEnabled" checked>
<label class="form-check-label small" for="navbarStickyEnabled" style="color: #495057;">
<i class="bi bi-pin-angle me-1" style="color: #FF8600;"></i>
<strong>Navbar fijo (sticky)</strong>
</label>
</div>
</div>
</div>
</div>
<!-- ========================================
GRUPO 2: LAYOUT Y ESTRUCTURA
======================================== -->
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
<div class="card-body">
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
<i class="bi bi-columns-gap me-2" style="color: #FF8600;"></i>
Layout y Estructura
</h5>
<!-- container_type (select) -->
<div class="mb-2">
<label for="navbarContainerType" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-box me-1" style="color: #FF8600;"></i>
Tipo de contenedor
</label>
<select id="navbarContainerType" class="form-select form-select-sm">
<option value="container" selected>Container (ancho fijo)</option>
<option value="container-fluid">Container Fluid (ancho completo)</option>
</select>
</div>
<!-- padding_vertical + z_index (compactados) -->
<div class="row g-2 mb-0">
<div class="col-6">
<label for="navbarPaddingVertical" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-arrows-vertical me-1" style="color: #FF8600;"></i>
Padding vertical
</label>
<input type="text" id="navbarPaddingVertical" class="form-control form-control-sm" value="0.75rem 0">
</div>
<div class="col-6">
<label for="navbarZIndex" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-layers me-1" style="color: #FF8600;"></i>
Z-index
</label>
<input type="number" id="navbarZIndex" class="form-control form-control-sm" value="1030" min="1" max="9999">
</div>
</div>
</div>
</div>
<!-- ========================================
GRUPO 3: CONFIGURACIÓN DEL MENÚ
======================================== -->
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
<div class="card-body">
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
<i class="bi bi-gear me-2" style="color: #FF8600;"></i>
Configuración del Menú
</h5>
<!-- menu_location + custom_menu_id (compactados) -->
<div class="row g-2 mb-2">
<div class="col-6">
<label for="navbarMenuLocation" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-pin-map me-1" style="color: #FF8600;"></i>
Ubicación del menú
</label>
<select id="navbarMenuLocation" class="form-select form-select-sm">
<option value="primary" selected>Menú Principal</option>
<option value="secondary">Menú Secundario</option>
<option value="custom">Menú personalizado</option>
</select>
</div>
<div class="col-6">
<label for="navbarCustomMenuId" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-hash me-1" style="color: #FF8600;"></i>
ID del menú
</label>
<input type="number" id="navbarCustomMenuId" class="form-control form-control-sm" value="0" min="0">
</div>
</div>
<!-- enable_dropdowns (switch) -->
<div class="mb-2">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="navbarEnableDropdowns" checked>
<label class="form-check-label small" for="navbarEnableDropdowns" style="color: #495057;">
<i class="bi bi-chevron-down me-1" style="color: #FF8600;"></i>
<strong>Habilitar submenús desplegables</strong>
</label>
</div>
</div>
<!-- mobile_breakpoint (select) -->
<div class="mb-0">
<label for="navbarMobileBreakpoint" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-phone-landscape me-1" style="color: #FF8600;"></i>
Breakpoint para menú móvil
</label>
<select id="navbarMobileBreakpoint" class="form-select form-select-sm">
<option value="sm">Small (576px)</option>
<option value="md">Medium (768px)</option>
<option value="lg" selected>Large (992px)</option>
<option value="xl">Extra Large (1200px)</option>
</select>
</div>
</div>
</div>
<!-- ========================================
GRUPO 4: LOGO/MARCA
======================================== -->
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
<div class="card-body">
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
<i class="bi bi-award me-2" style="color: #FF8600;"></i>
Logo/Marca
</h5>
<!-- show_brand (switch) -->
<div class="mb-2">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="navbarShowBrand">
<label class="form-check-label small" for="navbarShowBrand" style="color: #495057;">
<i class="bi bi-eye me-1" style="color: #FF8600;"></i>
<strong>Mostrar logo/marca</strong>
</label>
</div>
</div>
<!-- use_logo (switch) -->
<div class="mb-2">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="navbarUseLogo">
<label class="form-check-label small" for="navbarUseLogo" style="color: #495057;">
<i class="bi bi-image me-1" style="color: #FF8600;"></i>
<strong>Usar logo (imagen)</strong>
</label>
</div>
<small class="text-muted d-block ms-4 mt-1">Usa una imagen en lugar de texto</small>
</div>
<!-- logo_url + logo_height (compactados) -->
<div class="row g-2 mb-2">
<div class="col-8">
<label for="navbarLogoUrl" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-link-45deg me-1" style="color: #FF8600;"></i>
URL del logo
</label>
<input type="url" id="navbarLogoUrl" class="form-control form-control-sm" placeholder="https://...">
</div>
<div class="col-4">
<label for="navbarLogoHeight" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-arrows-vertical me-1" style="color: #FF8600;"></i>
Altura
</label>
<input type="text" id="navbarLogoHeight" class="form-control form-control-sm" value="40px">
</div>
</div>
<!-- brand_text -->
<div class="mb-2">
<label for="navbarBrandText" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-fonts me-1" style="color: #FF8600;"></i>
Texto de la marca
</label>
<input type="text" id="navbarBrandText" class="form-control form-control-sm" value="Mi Sitio" maxlength="50">
<small class="text-muted">Se muestra si no hay logo</small>
</div>
<!-- brand_font_size + brand_color (compactados) -->
<div class="row g-2 mb-2">
<div class="col-6">
<label for="navbarBrandFontSize" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-type me-1" style="color: #FF8600;"></i>
Tamaño fuente
</label>
<input type="text" id="navbarBrandFontSize" class="form-control form-control-sm" value="1.5rem">
</div>
<div class="col-6">
<label for="navbarBrandColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-palette me-1" style="color: #FF8600;"></i>
Color
</label>
<input type="color" id="navbarBrandColor" class="form-control form-control-color w-100" value="#FFFFFF">
</div>
</div>
<!-- brand_hover_color -->
<div class="mb-0">
<label for="navbarBrandHoverColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-hand-index me-1" style="color: #FF8600;"></i>
Color hover
</label>
<input type="color" id="navbarBrandHoverColor" class="form-control form-control-color w-100" value="#FF8600">
</div>
</div>
</div>
</div>
<div class="col-lg-6">
<!-- ========================================
GRUPO 5: ESTILOS DEL NAVBAR
======================================== -->
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
<div class="card-body">
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
<i class="bi bi-paint-bucket me-2" style="color: #FF8600;"></i>
Estilos del Navbar
</h5>
<!-- background_color -->
<div class="mb-2">
<label for="navbarBackgroundColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-palette me-1" style="color: #FF8600;"></i>
Color de fondo
</label>
<input type="color" id="navbarBackgroundColor" class="form-control form-control-color w-100" value="#1e3a5f">
<small class="text-muted d-block mt-1" id="navbarBackgroundColorValue">#1E3A5F</small>
</div>
<!-- box_shadow -->
<div class="mb-0">
<label for="navbarBoxShadow" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-droplet me-1" style="color: #FF8600;"></i>
Sombra del navbar
</label>
<input type="text" id="navbarBoxShadow" class="form-control form-control-sm" value="0 4px 12px rgba(30, 58, 95, 0.15)">
<small class="text-muted">Sombra CSS (ej: 0 4px 12px rgba(0,0,0,0.15))</small>
</div>
</div>
</div>
<!-- ========================================
GRUPO 6: ESTILOS DE ENLACES
======================================== -->
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
<div class="card-body">
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
<i class="bi bi-link-45deg me-2" style="color: #FF8600;"></i>
Estilos de Enlaces
</h5>
<!-- COLOR PICKERS EN GRID 3 COLORES -->
<div class="row g-2 mb-2">
<div class="col-4">
<label for="navbarTextColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-fonts me-1" style="color: #FF8600;"></i>
Color texto
</label>
<input type="color" id="navbarTextColor" class="form-control form-control-color w-100" value="#FFFFFF">
<small class="text-muted d-block mt-1" id="navbarTextColorValue">#FFFFFF</small>
</div>
<div class="col-4">
<label for="navbarHoverColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-hand-index me-1" style="color: #FF8600;"></i>
Color hover
</label>
<input type="color" id="navbarHoverColor" class="form-control form-control-color w-100" value="#FF8600">
<small class="text-muted d-block mt-1" id="navbarHoverColorValue">#FF8600</small>
</div>
<div class="col-4">
<label for="navbarActiveColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-check-circle me-1" style="color: #FF8600;"></i>
Color activo
</label>
<input type="color" id="navbarActiveColor" class="form-control form-control-color w-100" value="#FF8600">
<small class="text-muted d-block mt-1" id="navbarActiveColorValue">#FF8600</small>
</div>
</div>
<!-- font_size + font_weight (compactados) -->
<div class="row g-2 mb-2">
<div class="col-6">
<label for="navbarFontSize" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-type me-1" style="color: #FF8600;"></i>
Tamaño fuente
</label>
<input type="text" id="navbarFontSize" class="form-control form-control-sm" value="0.9rem">
</div>
<div class="col-6">
<label for="navbarFontWeight" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-fonts me-1" style="color: #FF8600;"></i>
Grosor fuente
</label>
<input type="number" id="navbarFontWeight" class="form-control form-control-sm" value="500" min="100" max="900" step="100">
</div>
</div>
<!-- padding + border_radius (compactados) -->
<div class="row g-2 mb-2">
<div class="col-6">
<label for="navbarLinkPadding" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-bounding-box me-1" style="color: #FF8600;"></i>
Padding
</label>
<input type="text" id="navbarLinkPadding" class="form-control form-control-sm" value="0.5rem 0.65rem">
</div>
<div class="col-6">
<label for="navbarBorderRadius" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-square me-1" style="color: #FF8600;"></i>
Border radius
</label>
<input type="text" id="navbarBorderRadius" class="form-control form-control-sm" value="4px">
</div>
</div>
<!-- show_underline_effect (switch) -->
<div class="mb-2">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="navbarShowUnderlineEffect" checked>
<label class="form-check-label small" for="navbarShowUnderlineEffect" style="color: #495057;">
<i class="bi bi-dash-lg me-1" style="color: #FF8600;"></i>
<strong>Mostrar efecto de subrayado</strong>
</label>
</div>
</div>
<!-- underline_color -->
<div class="mb-0">
<label for="navbarUnderlineColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-palette me-1" style="color: #FF8600;"></i>
Color del subrayado
</label>
<input type="color" id="navbarUnderlineColor" class="form-control form-control-color w-100" value="#FF8600">
<small class="text-muted d-block mt-1" id="navbarUnderlineColorValue">#FF8600</small>
</div>
</div>
</div>
<!-- ========================================
GRUPO 7: ESTILOS DE DROPDOWN
======================================== -->
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
<div class="card-body">
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
<i class="bi bi-chevron-down me-2" style="color: #FF8600;"></i>
Estilos de Dropdown
</h5>
<!-- background_color -->
<div class="mb-2">
<label for="navbarDropdownBackground" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-paint-bucket me-1" style="color: #FF8600;"></i>
Fondo dropdown
</label>
<input type="color" id="navbarDropdownBackground" class="form-control form-control-color w-100" value="#FFFFFF">
<small class="text-muted d-block mt-1" id="navbarDropdownBackgroundValue">#FFFFFF</small>
</div>
<!-- border_radius + shadow (compactados) -->
<div class="row g-2 mb-2">
<div class="col-6">
<label for="navbarDropdownBorderRadius" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-square me-1" style="color: #FF8600;"></i>
Border radius
</label>
<input type="text" id="navbarDropdownBorderRadius" class="form-control form-control-sm" value="8px">
</div>
<div class="col-6">
<label for="navbarDropdownShadow" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-droplet me-1" style="color: #FF8600;"></i>
Sombra
</label>
<input type="text" id="navbarDropdownShadow" class="form-control form-control-sm" value="0 8px 24px rgba(0,0,0,0.12)">
</div>
</div>
<!-- item_color + item_hover_background -->
<div class="row g-2 mb-2">
<div class="col-6">
<label for="navbarDropdownItemColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-fonts me-1" style="color: #FF8600;"></i>
Color items
</label>
<input type="color" id="navbarDropdownItemColor" class="form-control form-control-color w-100" value="#495057">
</div>
<div class="col-6">
<label for="navbarDropdownItemHoverBg" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-paint-bucket me-1" style="color: #FF8600;"></i>
Fondo hover
</label>
<input type="color" id="navbarDropdownItemHoverBg" class="form-control form-control-color w-100" value="#FFF5EB">
</div>
</div>
<!-- item_padding -->
<div class="mb-0">
<label for="navbarDropdownItemPadding" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-bounding-box me-1" style="color: #FF8600;"></i>
Padding de items
</label>
<input type="text" id="navbarDropdownItemPadding" class="form-control form-control-sm" value="0.625rem 1.25rem">
<small class="text-muted">Espaciado interno de items (ej: 0.625rem 1.25rem)</small>
</div>
</div>
</div>
</div>
</div>
</div><!-- /tab-pane -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Actualizar valores HEX de color pickers
document.querySelectorAll('input[type="color"]').forEach(picker => {
const valueDisplay = document.getElementById(picker.id + 'Value');
if (valueDisplay) {
picker.addEventListener('input', function() {
valueDisplay.textContent = this.value.toUpperCase();
});
}
});
// Simular reset button
document.getElementById('resetNavbarDefaults').addEventListener('click', function() {
if (confirm('¿Restaurar todos los valores a los valores por defecto?')) {
alert('En producción, esto restauraría los valores del schema JSON');
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,501 @@
<?php
declare(strict_types=1);
namespace ROITheme\Admin\RelatedPost\Infrastructure\Ui;
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
/**
* FormBuilder para Related Posts
*
* @package ROITheme\Admin\RelatedPost\Infrastructure\Ui
*/
final class RelatedPostFormBuilder
{
public function __construct(
private AdminDashboardRenderer $renderer
) {}
public function buildForm(string $componentId): string
{
$html = '';
$html .= $this->buildHeader($componentId);
$html .= '<div class="row g-3">';
// Columna izquierda
$html .= '<div class="col-lg-6">';
$html .= $this->buildVisibilityGroup($componentId);
$html .= $this->buildContentGroup($componentId);
$html .= $this->buildLayoutGroup($componentId);
$html .= '</div>';
// Columna derecha
$html .= '<div class="col-lg-6">';
$html .= $this->buildTypographyGroup($componentId);
$html .= $this->buildColorsGroup($componentId);
$html .= $this->buildSpacingGroup($componentId);
$html .= $this->buildEffectsGroup($componentId);
$html .= '</div>';
$html .= '</div>';
return $html;
}
private function buildHeader(string $componentId): string
{
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
$html .= ' <div>';
$html .= ' <h3 class="h4 mb-1 fw-bold">';
$html .= ' <i class="bi bi-grid-3x3-gap me-2" style="color: #FF8600;"></i>';
$html .= ' Configuracion de Posts Relacionados';
$html .= ' </h3>';
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
$html .= ' Seccion de posts relacionados con grid de cards';
$html .= ' </p>';
$html .= ' </div>';
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="related_post">';
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
$html .= ' Restaurar valores por defecto';
$html .= ' </button>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildVisibilityGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
$html .= ' Visibilidad';
$html .= ' </h5>';
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
$html .= $this->buildSwitch('relatedPostEnabled', 'Activar componente', 'bi-power', $enabled);
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
$html .= $this->buildSwitch('relatedPostShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop);
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
$html .= $this->buildSwitch('relatedPostShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
$html .= ' <div class="mb-0 mt-3">';
$html .= ' <label for="relatedPostShowOnPages" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
$html .= ' Mostrar en';
$html .= ' </label>';
$html .= ' <select id="relatedPostShowOnPages" class="form-select form-select-sm">';
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
$html .= ' </select>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildContentGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-card-text me-2" style="color: #FF8600;"></i>';
$html .= ' Contenido';
$html .= ' </h5>';
// Section Title
$sectionTitle = $this->renderer->getFieldValue($componentId, 'content', 'section_title', 'Descubre Mas Contenido');
$html .= ' <div class="mb-3">';
$html .= ' <label for="relatedPostSectionTitle" class="form-label small mb-1 fw-semibold">Titulo de seccion</label>';
$html .= ' <input type="text" id="relatedPostSectionTitle" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($sectionTitle) . '">';
$html .= ' </div>';
// Posts per page
$postsPerPage = $this->renderer->getFieldValue($componentId, 'content', 'posts_per_page', '12');
$html .= ' <div class="mb-3">';
$html .= ' <label for="relatedPostPerPage" class="form-label small mb-1 fw-semibold">Posts por pagina</label>';
$html .= ' <input type="number" id="relatedPostPerPage" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($postsPerPage) . '" min="1" max="50">';
$html .= ' </div>';
// Order by
$orderby = $this->renderer->getFieldValue($componentId, 'content', 'orderby', 'rand');
$html .= ' <div class="mb-3">';
$html .= ' <label for="relatedPostOrderby" class="form-label small mb-1 fw-semibold">Ordenar por</label>';
$html .= ' <select id="relatedPostOrderby" class="form-select form-select-sm">';
$html .= ' <option value="rand"' . ($orderby === 'rand' ? ' selected' : '') . '>Aleatorio</option>';
$html .= ' <option value="date"' . ($orderby === 'date' ? ' selected' : '') . '>Fecha</option>';
$html .= ' <option value="title"' . ($orderby === 'title' ? ' selected' : '') . '>Titulo</option>';
$html .= ' <option value="comment_count"' . ($orderby === 'comment_count' ? ' selected' : '') . '>Comentarios</option>';
$html .= ' <option value="menu_order"' . ($orderby === 'menu_order' ? ' selected' : '') . '>Orden de menu</option>';
$html .= ' </select>';
$html .= ' </div>';
// Order direction
$order = $this->renderer->getFieldValue($componentId, 'content', 'order', 'DESC');
$html .= ' <div class="mb-3">';
$html .= ' <label for="relatedPostOrder" class="form-label small mb-1 fw-semibold">Direccion</label>';
$html .= ' <select id="relatedPostOrder" class="form-select form-select-sm">';
$html .= ' <option value="DESC"' . ($order === 'DESC' ? ' selected' : '') . '>Descendente</option>';
$html .= ' <option value="ASC"' . ($order === 'ASC' ? ' selected' : '') . '>Ascendente</option>';
$html .= ' </select>';
$html .= ' </div>';
// Show pagination
$showPagination = $this->renderer->getFieldValue($componentId, 'content', 'show_pagination', true);
$html .= $this->buildSwitch('relatedPostShowPagination', 'Mostrar paginacion', 'bi-three-dots', $showPagination);
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildLayoutGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-grid me-2" style="color: #FF8600;"></i>';
$html .= ' Disposicion';
$html .= ' </h5>';
// Columns desktop
$colsDesktop = $this->renderer->getFieldValue($componentId, 'layout', 'columns_desktop', '3');
$html .= ' <div class="mb-3">';
$html .= ' <label for="relatedPostColsDesktop" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-display me-1" style="color: #FF8600;"></i>';
$html .= ' Columnas escritorio';
$html .= ' </label>';
$html .= ' <select id="relatedPostColsDesktop" class="form-select form-select-sm">';
$html .= ' <option value="2"' . ($colsDesktop === '2' ? ' selected' : '') . '>2 columnas</option>';
$html .= ' <option value="3"' . ($colsDesktop === '3' ? ' selected' : '') . '>3 columnas</option>';
$html .= ' <option value="4"' . ($colsDesktop === '4' ? ' selected' : '') . '>4 columnas</option>';
$html .= ' </select>';
$html .= ' </div>';
// Columns tablet
$colsTablet = $this->renderer->getFieldValue($componentId, 'layout', 'columns_tablet', '2');
$html .= ' <div class="mb-3">';
$html .= ' <label for="relatedPostColsTablet" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-tablet me-1" style="color: #FF8600;"></i>';
$html .= ' Columnas tablet';
$html .= ' </label>';
$html .= ' <select id="relatedPostColsTablet" class="form-select form-select-sm">';
$html .= ' <option value="1"' . ($colsTablet === '1' ? ' selected' : '') . '>1 columna</option>';
$html .= ' <option value="2"' . ($colsTablet === '2' ? ' selected' : '') . '>2 columnas</option>';
$html .= ' <option value="3"' . ($colsTablet === '3' ? ' selected' : '') . '>3 columnas</option>';
$html .= ' </select>';
$html .= ' </div>';
// Columns mobile
$colsMobile = $this->renderer->getFieldValue($componentId, 'layout', 'columns_mobile', '1');
$html .= ' <div class="mb-0">';
$html .= ' <label for="relatedPostColsMobile" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-phone me-1" style="color: #FF8600;"></i>';
$html .= ' Columnas movil';
$html .= ' </label>';
$html .= ' <select id="relatedPostColsMobile" class="form-select form-select-sm">';
$html .= ' <option value="1"' . ($colsMobile === '1' ? ' selected' : '') . '>1 columna</option>';
$html .= ' <option value="2"' . ($colsMobile === '2' ? ' selected' : '') . '>2 columnas</option>';
$html .= ' </select>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildTypographyGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-fonts me-2" style="color: #FF8600;"></i>';
$html .= ' Tipografia';
$html .= ' </h5>';
$html .= ' <div class="row g-2 mb-3">';
$sectionTitleSize = $this->renderer->getFieldValue($componentId, 'typography', 'section_title_size', '1.75rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="relatedPostSectionTitleSize" class="form-label small mb-1 fw-semibold">Tamano titulo seccion</label>';
$html .= ' <input type="text" id="relatedPostSectionTitleSize" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($sectionTitleSize) . '">';
$html .= ' </div>';
$sectionTitleWeight = $this->renderer->getFieldValue($componentId, 'typography', 'section_title_weight', '500');
$html .= ' <div class="col-6">';
$html .= ' <label for="relatedPostSectionTitleWeight" class="form-label small mb-1 fw-semibold">Peso titulo seccion</label>';
$html .= ' <input type="text" id="relatedPostSectionTitleWeight" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($sectionTitleWeight) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-0">';
$cardTitleSize = $this->renderer->getFieldValue($componentId, 'typography', 'card_title_size', '1rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="relatedPostCardTitleSize" class="form-label small mb-1 fw-semibold">Tamano titulo card</label>';
$html .= ' <input type="text" id="relatedPostCardTitleSize" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($cardTitleSize) . '">';
$html .= ' </div>';
$cardTitleWeight = $this->renderer->getFieldValue($componentId, 'typography', 'card_title_weight', '500');
$html .= ' <div class="col-6">';
$html .= ' <label for="relatedPostCardTitleWeight" class="form-label small mb-1 fw-semibold">Peso titulo card</label>';
$html .= ' <input type="text" id="relatedPostCardTitleWeight" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($cardTitleWeight) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildColorsGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
$html .= ' Colores';
$html .= ' </h5>';
// Seccion
$html .= ' <p class="small fw-semibold mb-2">Seccion</p>';
$html .= ' <div class="row g-2 mb-3">';
$sectionTitleColor = $this->renderer->getFieldValue($componentId, 'colors', 'section_title_color', '#212529');
$html .= $this->buildColorPicker('relatedPostSectionTitleColor', 'Titulo seccion', $sectionTitleColor);
$html .= ' </div>';
// Cards
$html .= ' <p class="small fw-semibold mb-2">Cards</p>';
$html .= ' <div class="row g-2 mb-3">';
$cardBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'card_bg_color', '#ffffff');
$html .= $this->buildColorPicker('relatedPostCardBgColor', 'Fondo card', $cardBgColor);
$cardTitleColor = $this->renderer->getFieldValue($componentId, 'colors', 'card_title_color', '#212529');
$html .= $this->buildColorPicker('relatedPostCardTitleColor', 'Titulo card', $cardTitleColor);
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-3">';
$cardHoverBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'card_hover_bg_color', '#f8f9fa');
$html .= $this->buildColorPicker('relatedPostCardHoverBgColor', 'Fondo hover', $cardHoverBgColor);
$html .= ' </div>';
// Paginacion
$html .= ' <p class="small fw-semibold mb-2">Paginacion</p>';
$html .= ' <div class="row g-2 mb-3">';
$paginationBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'pagination_bg_color', '#ffffff');
$html .= $this->buildColorPicker('relatedPostPaginationBgColor', 'Fondo', $paginationBgColor);
$paginationTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'pagination_text_color', '#0d6efd');
$html .= $this->buildColorPicker('relatedPostPaginationTextColor', 'Texto', $paginationTextColor);
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-0">';
$paginationActiveBg = $this->renderer->getFieldValue($componentId, 'colors', 'pagination_active_bg', '#0d6efd');
$html .= $this->buildColorPicker('relatedPostPaginationActiveBg', 'Activo fondo', $paginationActiveBg);
$paginationActiveText = $this->renderer->getFieldValue($componentId, 'colors', 'pagination_active_text', '#ffffff');
$html .= $this->buildColorPicker('relatedPostPaginationActiveText', 'Activo texto', $paginationActiveText);
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildSpacingGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-arrows-move me-2" style="color: #FF8600;"></i>';
$html .= ' Espaciado';
$html .= ' </h5>';
$html .= ' <div class="row g-2 mb-3">';
$sectionMarginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'section_margin_top', '3rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="relatedPostSectionMarginTop" class="form-label small mb-1 fw-semibold">Margen superior</label>';
$html .= ' <input type="text" id="relatedPostSectionMarginTop" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($sectionMarginTop) . '">';
$html .= ' </div>';
$sectionMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'section_margin_bottom', '3rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="relatedPostSectionMarginBottom" class="form-label small mb-1 fw-semibold">Margen inferior</label>';
$html .= ' <input type="text" id="relatedPostSectionMarginBottom" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($sectionMarginBottom) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-3">';
$titleMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'title_margin_bottom', '1.5rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="relatedPostTitleMarginBottom" class="form-label small mb-1 fw-semibold">Margen titulo</label>';
$html .= ' <input type="text" id="relatedPostTitleMarginBottom" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($titleMarginBottom) . '">';
$html .= ' </div>';
$gridGap = $this->renderer->getFieldValue($componentId, 'spacing', 'grid_gap', '1.5rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="relatedPostGridGap" class="form-label small mb-1 fw-semibold">Espacio cards</label>';
$html .= ' <input type="text" id="relatedPostGridGap" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($gridGap) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-0">';
$cardPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'card_padding', '1.5rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="relatedPostCardPadding" class="form-label small mb-1 fw-semibold">Padding card</label>';
$html .= ' <input type="text" id="relatedPostCardPadding" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($cardPadding) . '">';
$html .= ' </div>';
$paginationMarginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'pagination_margin_top', '1rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="relatedPostPaginationMarginTop" class="form-label small mb-1 fw-semibold">Margen paginacion</label>';
$html .= ' <input type="text" id="relatedPostPaginationMarginTop" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($paginationMarginTop) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildEffectsGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-magic me-2" style="color: #FF8600;"></i>';
$html .= ' Efectos Visuales';
$html .= ' </h5>';
$html .= ' <div class="row g-2 mb-3">';
$cardBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_border_radius', '0.375rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="relatedPostCardBorderRadius" class="form-label small mb-1 fw-semibold">Radio borde card</label>';
$html .= ' <input type="text" id="relatedPostCardBorderRadius" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($cardBorderRadius) . '">';
$html .= ' </div>';
$cardTransition = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_transition', '0.3s ease');
$html .= ' <div class="col-6">';
$html .= ' <label for="relatedPostCardTransition" class="form-label small mb-1 fw-semibold">Transicion</label>';
$html .= ' <input type="text" id="relatedPostCardTransition" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($cardTransition) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' <div class="mb-3">';
$cardShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_shadow', '0 .125rem .25rem rgba(0,0,0,.075)');
$html .= ' <label for="relatedPostCardShadow" class="form-label small mb-1 fw-semibold">Sombra card</label>';
$html .= ' <input type="text" id="relatedPostCardShadow" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($cardShadow) . '">';
$html .= ' </div>';
$html .= ' <div class="mb-0">';
$cardHoverShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_hover_shadow', '0 .5rem 1rem rgba(0,0,0,.15)');
$html .= ' <label for="relatedPostCardHoverShadow" class="form-label small mb-1 fw-semibold">Sombra hover</label>';
$html .= ' <input type="text" id="relatedPostCardHoverShadow" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($cardHoverShadow) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildSwitch(string $id, string $label, string $icon, mixed $checked): string
{
$checked = $checked === true || $checked === '1' || $checked === 1;
$html = ' <div class="mb-2">';
$html .= ' <div class="form-check form-switch">';
$html .= sprintf(
' <input class="form-check-input" type="checkbox" id="%s" %s>',
esc_attr($id),
$checked ? 'checked' : ''
);
$html .= sprintf(
' <label class="form-check-label small" for="%s">',
esc_attr($id)
);
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
$html .= sprintf(' <strong>%s</strong>', esc_html($label));
$html .= ' </label>';
$html .= ' </div>';
$html .= ' </div>';
return $html;
}
private function buildColorPicker(string $id, string $label, string $value): string
{
$html = ' <div class="col-6">';
$html .= sprintf(
' <label class="form-label small fw-semibold">%s</label>',
esc_html($label)
);
$html .= ' <div class="input-group input-group-sm">';
$html .= sprintf(
' <input type="color" class="form-control form-control-color" id="%s" value="%s">',
esc_attr($id),
esc_attr($value)
);
$html .= sprintf(
' <span class="input-group-text" id="%sValue">%s</span>',
esc_attr($id),
esc_html(strtoupper($value))
);
$html .= ' </div>';
$html .= ' </div>';
return $html;
}
}

View File

@@ -0,0 +1,529 @@
<?php
declare(strict_types=1);
namespace ROITheme\Admin\SocialShare\Infrastructure\Ui;
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
/**
* FormBuilder para Social Share
*
* Responsabilidad:
* - Generar HTML del formulario de configuracion
* - Usar Design System (Bootstrap 5)
* - Cargar valores desde BD via AdminDashboardRenderer
*
* @package ROITheme\Admin\SocialShare\Infrastructure\Ui
*/
final class SocialShareFormBuilder
{
public function __construct(
private AdminDashboardRenderer $renderer
) {}
public function buildForm(string $componentId): string
{
$html = '';
$html .= $this->buildHeader($componentId);
$html .= '<div class="row g-3">';
// Columna izquierda
$html .= '<div class="col-lg-6">';
$html .= $this->buildVisibilityGroup($componentId);
$html .= $this->buildContentGroup($componentId);
$html .= $this->buildEffectsGroup($componentId);
$html .= $this->buildTypographyGroup($componentId);
$html .= $this->buildSpacingGroup($componentId);
$html .= '</div>';
// Columna derecha
$html .= '<div class="col-lg-6">';
$html .= $this->buildNetworksGroup($componentId);
$html .= $this->buildColorsGroup($componentId);
$html .= '</div>';
$html .= '</div>';
return $html;
}
private function buildHeader(string $componentId): string
{
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
$html .= ' <div>';
$html .= ' <h3 class="h4 mb-1 fw-bold">';
$html .= ' <i class="bi bi-share me-2" style="color: #FF8600;"></i>';
$html .= ' Configuracion de Compartir en Redes';
$html .= ' </h3>';
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
$html .= ' Botones para compartir contenido en redes sociales';
$html .= ' </p>';
$html .= ' </div>';
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="social_share">';
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
$html .= ' Restaurar valores por defecto';
$html .= ' </button>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildVisibilityGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
$html .= ' Visibilidad';
$html .= ' </h5>';
// is_enabled
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
$html .= $this->buildSwitch('socialShareEnabled', 'Activar componente', 'bi-power', $enabled);
// show_on_desktop
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
$html .= $this->buildSwitch('socialShareShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop);
// show_on_mobile
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
$html .= $this->buildSwitch('socialShareShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
// show_on_pages
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
$html .= ' <div class="mb-0 mt-3">';
$html .= ' <label for="socialShareShowOnPages" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
$html .= ' Mostrar en';
$html .= ' </label>';
$html .= ' <select id="socialShareShowOnPages" class="form-select form-select-sm">';
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
$html .= ' </select>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildContentGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-card-text me-2" style="color: #FF8600;"></i>';
$html .= ' Contenido';
$html .= ' </h5>';
// show_label
$showLabel = $this->renderer->getFieldValue($componentId, 'content', 'show_label', true);
$html .= $this->buildSwitch('socialShareShowLabel', 'Mostrar etiqueta', 'bi-tag', $showLabel);
// label_text
$labelText = $this->renderer->getFieldValue($componentId, 'content', 'label_text', 'Compartir:');
$html .= ' <div class="mb-0 mt-3">';
$html .= ' <label for="socialShareLabelText" class="form-label small mb-1 fw-semibold">Texto etiqueta</label>';
$html .= ' <input type="text" id="socialShareLabelText" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($labelText) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildNetworksGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-globe me-2" style="color: #FF8600;"></i>';
$html .= ' Redes Sociales';
$html .= ' </h5>';
$html .= ' <p class="small text-muted mb-3">Configura las redes sociales y sus URLs</p>';
// Facebook
$showFacebook = $this->renderer->getFieldValue($componentId, 'networks', 'show_facebook', true);
$facebookUrl = $this->renderer->getFieldValue($componentId, 'networks', 'facebook_url', '');
$html .= $this->buildNetworkField('socialShareFacebook', 'socialShareFacebookUrl', 'Facebook', 'bi-facebook', $showFacebook, $facebookUrl, 'https://facebook.com/tu-pagina');
// Instagram
$showInstagram = $this->renderer->getFieldValue($componentId, 'networks', 'show_instagram', true);
$instagramUrl = $this->renderer->getFieldValue($componentId, 'networks', 'instagram_url', '');
$html .= $this->buildNetworkField('socialShareInstagram', 'socialShareInstagramUrl', 'Instagram', 'bi-instagram', $showInstagram, $instagramUrl, 'https://instagram.com/tu-perfil');
// LinkedIn
$showLinkedin = $this->renderer->getFieldValue($componentId, 'networks', 'show_linkedin', true);
$linkedinUrl = $this->renderer->getFieldValue($componentId, 'networks', 'linkedin_url', '');
$html .= $this->buildNetworkField('socialShareLinkedin', 'socialShareLinkedinUrl', 'LinkedIn', 'bi-linkedin', $showLinkedin, $linkedinUrl, 'https://linkedin.com/in/tu-perfil');
// WhatsApp
$showWhatsapp = $this->renderer->getFieldValue($componentId, 'networks', 'show_whatsapp', true);
$whatsappNumber = $this->renderer->getFieldValue($componentId, 'networks', 'whatsapp_number', '');
$html .= $this->buildNetworkField('socialShareWhatsapp', 'socialShareWhatsappNumber', 'WhatsApp', 'bi-whatsapp', $showWhatsapp, $whatsappNumber, '521234567890');
// X (Twitter)
$showTwitter = $this->renderer->getFieldValue($componentId, 'networks', 'show_twitter', true);
$twitterUrl = $this->renderer->getFieldValue($componentId, 'networks', 'twitter_url', '');
$html .= $this->buildNetworkField('socialShareTwitter', 'socialShareTwitterUrl', 'X (Twitter)', 'bi-twitter-x', $showTwitter, $twitterUrl, 'https://x.com/tu-perfil');
// Email
$showEmail = $this->renderer->getFieldValue($componentId, 'networks', 'show_email', true);
$emailAddress = $this->renderer->getFieldValue($componentId, 'networks', 'email_address', '');
$html .= $this->buildNetworkField('socialShareEmail', 'socialShareEmailAddress', 'Email', 'bi-envelope', $showEmail, $emailAddress, 'contacto@tudominio.com');
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildNetworkField(string $switchId, string $urlId, string $label, string $icon, mixed $checked, string $urlValue, string $placeholder): string
{
// Normalizar valor booleano desde BD
$checked = $checked === true || $checked === '1' || $checked === 1;
$html = ' <div class="mb-3 p-2 rounded" style="background-color: #f8f9fa;">';
// Switch
$html .= ' <div class="form-check form-switch">';
$html .= sprintf(
' <input class="form-check-input" type="checkbox" id="%s" %s>',
esc_attr($switchId),
$checked ? 'checked' : ''
);
$html .= sprintf(
' <label class="form-check-label small fw-semibold" for="%s">',
esc_attr($switchId)
);
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
$html .= sprintf(' %s', esc_html($label));
$html .= ' </label>';
$html .= ' </div>';
// URL Input
$html .= sprintf(
' <input type="text" id="%s" class="form-control form-control-sm mt-2" value="%s" placeholder="%s">',
esc_attr($urlId),
esc_attr($urlValue),
esc_attr($placeholder)
);
$html .= ' </div>';
return $html;
}
private function buildColorsGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
$html .= ' Colores';
$html .= ' </h5>';
// Colores generales
$html .= ' <p class="small fw-semibold mb-2">General</p>';
$html .= ' <div class="row g-2 mb-3">';
$labelColor = $this->renderer->getFieldValue($componentId, 'colors', 'label_color', '#6c757d');
$html .= $this->buildColorPicker('socialShareLabelColor', 'Etiqueta', $labelColor);
$borderTopColor = $this->renderer->getFieldValue($componentId, 'colors', 'border_top_color', '#dee2e6');
$html .= $this->buildColorPicker('socialShareBorderTopColor', 'Borde superior', $borderTopColor);
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-3">';
$buttonBackground = $this->renderer->getFieldValue($componentId, 'colors', 'button_background', '#ffffff');
$html .= $this->buildColorPicker('socialShareButtonBg', 'Fondo botones', $buttonBackground);
$html .= ' </div>';
// Colores por red social
$html .= ' <p class="small fw-semibold mb-2">Redes Sociales</p>';
$html .= ' <div class="row g-2 mb-3">';
$facebookColor = $this->renderer->getFieldValue($componentId, 'colors', 'facebook_color', '#0d6efd');
$html .= $this->buildColorPicker('socialShareFacebookColor', 'Facebook', $facebookColor);
$instagramColor = $this->renderer->getFieldValue($componentId, 'colors', 'instagram_color', '#dc3545');
$html .= $this->buildColorPicker('socialShareInstagramColor', 'Instagram', $instagramColor);
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-3">';
$linkedinColor = $this->renderer->getFieldValue($componentId, 'colors', 'linkedin_color', '#0dcaf0');
$html .= $this->buildColorPicker('socialShareLinkedinColor', 'LinkedIn', $linkedinColor);
$whatsappColor = $this->renderer->getFieldValue($componentId, 'colors', 'whatsapp_color', '#198754');
$html .= $this->buildColorPicker('socialShareWhatsappColor', 'WhatsApp', $whatsappColor);
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-0">';
$twitterColor = $this->renderer->getFieldValue($componentId, 'colors', 'twitter_color', '#212529');
$html .= $this->buildColorPicker('socialShareTwitterColor', 'X (Twitter)', $twitterColor);
$emailColor = $this->renderer->getFieldValue($componentId, 'colors', 'email_color', '#6c757d');
$html .= $this->buildColorPicker('socialShareEmailColor', 'Email', $emailColor);
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildTypographyGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-fonts me-2" style="color: #FF8600;"></i>';
$html .= ' Tipografia';
$html .= ' </h5>';
$html .= ' <div class="row g-2 mb-0">';
// label_font_size
$labelFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'label_font_size', '1rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="socialShareLabelFontSize" class="form-label small mb-1 fw-semibold">Tamano etiqueta</label>';
$html .= ' <input type="text" id="socialShareLabelFontSize" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($labelFontSize) . '">';
$html .= ' </div>';
// icon_font_size
$iconFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'icon_font_size', '1rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="socialShareIconFontSize" class="form-label small mb-1 fw-semibold">Tamano iconos</label>';
$html .= ' <input type="text" id="socialShareIconFontSize" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($iconFontSize) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildSpacingGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-arrows-move me-2" style="color: #FF8600;"></i>';
$html .= ' Espaciado';
$html .= ' </h5>';
$html .= ' <div class="row g-2 mb-3">';
// container_margin_top
$containerMarginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'container_margin_top', '3rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="socialShareMarginTop" class="form-label small mb-1 fw-semibold">Margen superior</label>';
$html .= ' <input type="text" id="socialShareMarginTop" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($containerMarginTop) . '">';
$html .= ' </div>';
// container_margin_bottom
$containerMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'container_margin_bottom', '3rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="socialShareMarginBottom" class="form-label small mb-1 fw-semibold">Margen inferior</label>';
$html .= ' <input type="text" id="socialShareMarginBottom" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($containerMarginBottom) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-3">';
// container_padding_top
$containerPaddingTop = $this->renderer->getFieldValue($componentId, 'spacing', 'container_padding_top', '1.5rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="socialSharePaddingTop" class="form-label small mb-1 fw-semibold">Padding superior</label>';
$html .= ' <input type="text" id="socialSharePaddingTop" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($containerPaddingTop) . '">';
$html .= ' </div>';
// container_padding_bottom
$containerPaddingBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'container_padding_bottom', '1.5rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="socialSharePaddingBottom" class="form-label small mb-1 fw-semibold">Padding inferior</label>';
$html .= ' <input type="text" id="socialSharePaddingBottom" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($containerPaddingBottom) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-3">';
// label_margin_bottom
$labelMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'label_margin_bottom', '1rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="socialShareLabelMarginBottom" class="form-label small mb-1 fw-semibold">Margen etiqueta</label>';
$html .= ' <input type="text" id="socialShareLabelMarginBottom" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($labelMarginBottom) . '">';
$html .= ' </div>';
// buttons_gap
$buttonsGap = $this->renderer->getFieldValue($componentId, 'spacing', 'buttons_gap', '0.5rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="socialShareButtonsGap" class="form-label small mb-1 fw-semibold">Espacio botones</label>';
$html .= ' <input type="text" id="socialShareButtonsGap" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($buttonsGap) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-0">';
// button_padding
$buttonPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'button_padding', '0.25rem 0.5rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="socialShareButtonPadding" class="form-label small mb-1 fw-semibold">Padding botones</label>';
$html .= ' <input type="text" id="socialShareButtonPadding" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($buttonPadding) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildEffectsGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-magic me-2" style="color: #FF8600;"></i>';
$html .= ' Efectos Visuales';
$html .= ' </h5>';
$html .= ' <div class="row g-2 mb-3">';
// border_top_width
$borderTopWidth = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_top_width', '1px');
$html .= ' <div class="col-6">';
$html .= ' <label for="socialShareBorderTopWidth" class="form-label small mb-1 fw-semibold">Grosor borde sup.</label>';
$html .= ' <input type="text" id="socialShareBorderTopWidth" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($borderTopWidth) . '">';
$html .= ' </div>';
// button_border_width
$buttonBorderWidth = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_border_width', '2px');
$html .= ' <div class="col-6">';
$html .= ' <label for="socialShareButtonBorderWidth" class="form-label small mb-1 fw-semibold">Grosor borde btn</label>';
$html .= ' <input type="text" id="socialShareButtonBorderWidth" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($buttonBorderWidth) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-3">';
// button_border_radius
$buttonBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_border_radius', '0.375rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="socialShareButtonBorderRadius" class="form-label small mb-1 fw-semibold">Radio botones</label>';
$html .= ' <input type="text" id="socialShareButtonBorderRadius" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($buttonBorderRadius) . '">';
$html .= ' </div>';
// transition_duration
$transitionDuration = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s');
$html .= ' <div class="col-6">';
$html .= ' <label for="socialShareTransitionDuration" class="form-label small mb-1 fw-semibold">Duracion transicion</label>';
$html .= ' <input type="text" id="socialShareTransitionDuration" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($transitionDuration) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-0">';
// hover_box_shadow
$hoverBoxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'hover_box_shadow', '0 4px 12px rgba(0, 0, 0, 0.15)');
$html .= ' <div class="col-12">';
$html .= ' <label for="socialShareHoverBoxShadow" class="form-label small mb-1 fw-semibold">Sombra hover</label>';
$html .= ' <input type="text" id="socialShareHoverBoxShadow" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($hoverBoxShadow) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildSwitch(string $id, string $label, string $icon, mixed $checked): string
{
// Normalizar valor booleano desde BD
$checked = $checked === true || $checked === '1' || $checked === 1;
$html = ' <div class="mb-2">';
$html .= ' <div class="form-check form-switch">';
$html .= sprintf(
' <input class="form-check-input" type="checkbox" id="%s" %s>',
esc_attr($id),
$checked ? 'checked' : ''
);
$html .= sprintf(
' <label class="form-check-label small" for="%s">',
esc_attr($id)
);
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
$html .= sprintf(' <strong>%s</strong>', esc_html($label));
$html .= ' </label>';
$html .= ' </div>';
$html .= ' </div>';
return $html;
}
private function buildColorPicker(string $id, string $label, string $value): string
{
$html = ' <div class="col-6">';
$html .= sprintf(
' <label class="form-label small fw-semibold">%s</label>',
esc_html($label)
);
$html .= ' <div class="input-group input-group-sm">';
$html .= sprintf(
' <input type="color" class="form-control form-control-color" id="%s" value="%s">',
esc_attr($id),
esc_attr($value)
);
$html .= sprintf(
' <span class="input-group-text" id="%sValue">%s</span>',
esc_attr($id),
esc_html(strtoupper($value))
);
$html .= ' </div>';
$html .= ' </div>';
return $html;
}
}

View File

@@ -0,0 +1,588 @@
<?php
declare(strict_types=1);
namespace ROITheme\Admin\TableOfContents\Infrastructure\Ui;
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
/**
* FormBuilder para la Tabla de Contenido
*
* Responsabilidad:
* - Generar HTML del formulario de configuracion
* - Usar Design System (Bootstrap 5)
* - Cargar valores desde BD via AdminDashboardRenderer
*
* @package ROITheme\Admin\TableOfContents\Infrastructure\Ui
*/
final class TableOfContentsFormBuilder
{
public function __construct(
private AdminDashboardRenderer $renderer
) {}
public function buildForm(string $componentId): string
{
$html = '';
$html .= $this->buildHeader($componentId);
$html .= '<div class="row g-3">';
// Columna izquierda
$html .= '<div class="col-lg-6">';
$html .= $this->buildVisibilityGroup($componentId);
$html .= $this->buildContentGroup($componentId);
$html .= $this->buildBehaviorGroup($componentId);
$html .= $this->buildEffectsGroup($componentId);
$html .= '</div>';
// Columna derecha
$html .= '<div class="col-lg-6">';
$html .= $this->buildTypographyGroup($componentId);
$html .= $this->buildColorsGroup($componentId);
$html .= $this->buildSpacingGroup($componentId);
$html .= '</div>';
$html .= '</div>';
return $html;
}
private function buildHeader(string $componentId): string
{
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
$html .= ' <div>';
$html .= ' <h3 class="h4 mb-1 fw-bold">';
$html .= ' <i class="bi bi-list-nested me-2" style="color: #FF8600;"></i>';
$html .= ' Configuracion de Tabla de Contenido';
$html .= ' </h3>';
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
$html .= ' Navegacion automatica con ScrollSpy';
$html .= ' </p>';
$html .= ' </div>';
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="table_of_contents">';
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
$html .= ' Restaurar valores por defecto';
$html .= ' </button>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildVisibilityGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
$html .= ' Visibilidad';
$html .= ' </h5>';
// is_enabled
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
$html .= $this->buildSwitch('tocEnabled', 'Activar tabla de contenido', 'bi-power', $enabled);
// show_on_desktop
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
$html .= $this->buildSwitch('tocShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop);
// show_on_mobile
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', false);
$html .= $this->buildSwitch('tocShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
// show_on_pages
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
$html .= ' <div class="mb-0 mt-3">';
$html .= ' <label for="tocShowOnPages" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
$html .= ' Mostrar en';
$html .= ' </label>';
$html .= ' <select id="tocShowOnPages" class="form-select form-select-sm">';
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las paginas</option>';
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts</option>';
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo paginas</option>';
$html .= ' </select>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildContentGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-card-text me-2" style="color: #FF8600;"></i>';
$html .= ' Contenido';
$html .= ' </h5>';
// title
$title = $this->renderer->getFieldValue($componentId, 'content', 'title', 'Tabla de Contenido');
$html .= ' <div class="mb-3">';
$html .= ' <label for="tocTitle" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-type me-1" style="color: #FF8600;"></i>';
$html .= ' Titulo';
$html .= ' </label>';
$html .= ' <input type="text" id="tocTitle" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($title) . '" placeholder="Tabla de Contenido">';
$html .= ' </div>';
// auto_generate
$autoGenerate = $this->renderer->getFieldValue($componentId, 'content', 'auto_generate', true);
$html .= $this->buildSwitch('tocAutoGenerate', 'Generar automaticamente', 'bi-magic', $autoGenerate);
$html .= ' <small class="text-muted d-block mb-3">Genera TOC desde los encabezados del contenido</small>';
// heading_levels
$headingLevels = $this->renderer->getFieldValue($componentId, 'content', 'heading_levels', 'h2,h3');
$html .= ' <div class="mb-3">';
$html .= ' <label for="tocHeadingLevels" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-list-ol me-1" style="color: #FF8600;"></i>';
$html .= ' Niveles de encabezados';
$html .= ' </label>';
$html .= ' <input type="text" id="tocHeadingLevels" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($headingLevels) . '" placeholder="h2,h3">';
$html .= ' <small class="text-muted">Separados por coma: h2,h3,h4</small>';
$html .= ' </div>';
// smooth_scroll
$smoothScroll = $this->renderer->getFieldValue($componentId, 'content', 'smooth_scroll', true);
$html .= $this->buildSwitch('tocSmoothScroll', 'Scroll suave', 'bi-arrow-down-circle', $smoothScroll);
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildBehaviorGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-gear me-2" style="color: #FF8600;"></i>';
$html .= ' Comportamiento';
$html .= ' </h5>';
// is_sticky
$isSticky = $this->renderer->getFieldValue($componentId, 'behavior', 'is_sticky', true);
$html .= $this->buildSwitch('tocIsSticky', 'Sticky (fijo al scroll)', 'bi-pin', $isSticky);
// scroll_offset
$scrollOffset = $this->renderer->getFieldValue($componentId, 'behavior', 'scroll_offset', '100');
$html .= ' <div class="mb-3">';
$html .= ' <label for="tocScrollOffset" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-arrows-vertical me-1" style="color: #FF8600;"></i>';
$html .= ' Offset de scroll (px)';
$html .= ' </label>';
$html .= ' <input type="text" id="tocScrollOffset" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($scrollOffset) . '" placeholder="100">';
$html .= ' </div>';
// max_height
$maxHeight = $this->renderer->getFieldValue($componentId, 'behavior', 'max_height', 'calc(100vh - 71px - 10px - 250px - 15px - 15px)');
$html .= ' <div class="mb-0">';
$html .= ' <label for="tocMaxHeight" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-arrows-expand me-1" style="color: #FF8600;"></i>';
$html .= ' Altura maxima';
$html .= ' </label>';
$html .= ' <input type="text" id="tocMaxHeight" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($maxHeight) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildTypographyGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-fonts me-2" style="color: #FF8600;"></i>';
$html .= ' Tipografia';
$html .= ' </h5>';
$html .= ' <div class="row g-2 mb-3">';
// title_font_size
$titleFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_size', '1rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="tocTitleFontSize" class="form-label small mb-1 fw-semibold">Tamano titulo</label>';
$html .= ' <input type="text" id="tocTitleFontSize" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($titleFontSize) . '" placeholder="1rem">';
$html .= ' </div>';
// title_font_weight
$titleFontWeight = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_weight', '600');
$html .= ' <div class="col-6">';
$html .= ' <label for="tocTitleFontWeight" class="form-label small mb-1 fw-semibold">Peso titulo</label>';
$html .= ' <input type="text" id="tocTitleFontWeight" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($titleFontWeight) . '" placeholder="600">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-3">';
// link_font_size
$linkFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'link_font_size', '0.9rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="tocLinkFontSize" class="form-label small mb-1 fw-semibold">Tamano enlaces</label>';
$html .= ' <input type="text" id="tocLinkFontSize" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($linkFontSize) . '" placeholder="0.9rem">';
$html .= ' </div>';
// link_line_height
$linkLineHeight = $this->renderer->getFieldValue($componentId, 'typography', 'link_line_height', '1.3');
$html .= ' <div class="col-6">';
$html .= ' <label for="tocLinkLineHeight" class="form-label small mb-1 fw-semibold">Altura linea</label>';
$html .= ' <input type="text" id="tocLinkLineHeight" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($linkLineHeight) . '" placeholder="1.3">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-0">';
// level_three_font_size
$level3FontSize = $this->renderer->getFieldValue($componentId, 'typography', 'level_three_font_size', '0.85rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="tocLevelThreeFontSize" class="form-label small mb-1 fw-semibold">Tamano H3</label>';
$html .= ' <input type="text" id="tocLevelThreeFontSize" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($level3FontSize) . '" placeholder="0.85rem">';
$html .= ' </div>';
// level_four_font_size
$level4FontSize = $this->renderer->getFieldValue($componentId, 'typography', 'level_four_font_size', '0.8rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="tocLevelFourFontSize" class="form-label small mb-1 fw-semibold">Tamano H4</label>';
$html .= ' <input type="text" id="tocLevelFourFontSize" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($level4FontSize) . '" placeholder="0.8rem">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildColorsGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
$html .= ' Colores';
$html .= ' </h5>';
// Colores principales
$html .= ' <p class="small fw-semibold mb-2">Contenedor</p>';
$html .= ' <div class="row g-2 mb-3">';
$bgColor = $this->renderer->getFieldValue($componentId, 'colors', 'background_color', '#ffffff');
$html .= $this->buildColorPicker('tocBackgroundColor', 'Fondo', $bgColor);
$borderColor = $this->renderer->getFieldValue($componentId, 'colors', 'border_color', '#E6E9ED');
$html .= $this->buildColorPicker('tocBorderColor', 'Borde', $borderColor);
$html .= ' </div>';
// Colores del titulo
$html .= ' <p class="small fw-semibold mb-2">Titulo</p>';
$html .= ' <div class="row g-2 mb-3">';
$titleColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_color', '#0E2337');
$html .= $this->buildColorPicker('tocTitleColor', 'Color', $titleColor);
$titleBorderColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_border_color', '#E6E9ED');
$html .= $this->buildColorPicker('tocTitleBorderColor', 'Borde', $titleBorderColor);
$html .= ' </div>';
// Colores de enlaces
$html .= ' <p class="small fw-semibold mb-2">Enlaces</p>';
$html .= ' <div class="row g-2 mb-3">';
$linkColor = $this->renderer->getFieldValue($componentId, 'colors', 'link_color', '#6B7280');
$html .= $this->buildColorPicker('tocLinkColor', 'Normal', $linkColor);
$linkHoverColor = $this->renderer->getFieldValue($componentId, 'colors', 'link_hover_color', '#0E2337');
$html .= $this->buildColorPicker('tocLinkHoverColor', 'Hover', $linkHoverColor);
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-3">';
$linkHoverBg = $this->renderer->getFieldValue($componentId, 'colors', 'link_hover_background', '#F9FAFB');
$html .= $this->buildColorPicker('tocLinkHoverBackground', 'Fondo hover', $linkHoverBg);
$activeBorderColor = $this->renderer->getFieldValue($componentId, 'colors', 'active_border_color', '#0E2337');
$html .= $this->buildColorPicker('tocActiveBorderColor', 'Borde activo', $activeBorderColor);
$html .= ' </div>';
// Colores de activo
$html .= ' <p class="small fw-semibold mb-2">Estado Activo</p>';
$html .= ' <div class="row g-2 mb-3">';
$activeBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'active_background_color', '#F9FAFB');
$html .= $this->buildColorPicker('tocActiveBackgroundColor', 'Fondo', $activeBgColor);
$activeTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'active_text_color', '#0E2337');
$html .= $this->buildColorPicker('tocActiveTextColor', 'Texto', $activeTextColor);
$html .= ' </div>';
// Colores de scrollbar
$html .= ' <p class="small fw-semibold mb-2">Scrollbar</p>';
$html .= ' <div class="row g-2 mb-0">';
$scrollbarTrack = $this->renderer->getFieldValue($componentId, 'colors', 'scrollbar_track_color', '#F9FAFB');
$html .= $this->buildColorPicker('tocScrollbarTrackColor', 'Pista', $scrollbarTrack);
$scrollbarThumb = $this->renderer->getFieldValue($componentId, 'colors', 'scrollbar_thumb_color', '#6B7280');
$html .= $this->buildColorPicker('tocScrollbarThumbColor', 'Thumb', $scrollbarThumb);
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildSpacingGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-arrows-move me-2" style="color: #FF8600;"></i>';
$html .= ' Espaciado';
$html .= ' </h5>';
$html .= ' <div class="row g-2 mb-3">';
// container_padding
$containerPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'container_padding', '12px 16px');
$html .= ' <div class="col-6">';
$html .= ' <label for="tocContainerPadding" class="form-label small mb-1 fw-semibold">Padding contenedor</label>';
$html .= ' <input type="text" id="tocContainerPadding" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($containerPadding) . '">';
$html .= ' </div>';
// margin_bottom
$marginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_bottom', '13px');
$html .= ' <div class="col-6">';
$html .= ' <label for="tocMarginBottom" class="form-label small mb-1 fw-semibold">Margen inferior</label>';
$html .= ' <input type="text" id="tocMarginBottom" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($marginBottom) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-3">';
// title_padding_bottom
$titlePaddingBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'title_padding_bottom', '8px');
$html .= ' <div class="col-6">';
$html .= ' <label for="tocTitlePaddingBottom" class="form-label small mb-1 fw-semibold">Padding titulo</label>';
$html .= ' <input type="text" id="tocTitlePaddingBottom" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($titlePaddingBottom) . '">';
$html .= ' </div>';
// title_margin_bottom
$titleMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'title_margin_bottom', '0.75rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="tocTitleMarginBottom" class="form-label small mb-1 fw-semibold">Margen titulo</label>';
$html .= ' <input type="text" id="tocTitleMarginBottom" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($titleMarginBottom) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-3">';
// item_margin_bottom
$itemMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'item_margin_bottom', '0.15rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="tocItemMarginBottom" class="form-label small mb-1 fw-semibold">Margen items</label>';
$html .= ' <input type="text" id="tocItemMarginBottom" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($itemMarginBottom) . '">';
$html .= ' </div>';
// link_padding
$linkPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'link_padding', '0.3rem 0.85rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="tocLinkPadding" class="form-label small mb-1 fw-semibold">Padding enlaces</label>';
$html .= ' <input type="text" id="tocLinkPadding" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($linkPadding) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-0">';
// level_three_padding_left
$level3PaddingLeft = $this->renderer->getFieldValue($componentId, 'spacing', 'level_three_padding_left', '1.5rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="tocLevelThreePaddingLeft" class="form-label small mb-1 fw-semibold">Padding H3</label>';
$html .= ' <input type="text" id="tocLevelThreePaddingLeft" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($level3PaddingLeft) . '">';
$html .= ' </div>';
// level_four_padding_left
$level4PaddingLeft = $this->renderer->getFieldValue($componentId, 'spacing', 'level_four_padding_left', '2rem');
$html .= ' <div class="col-6">';
$html .= ' <label for="tocLevelFourPaddingLeft" class="form-label small mb-1 fw-semibold">Padding H4</label>';
$html .= ' <input type="text" id="tocLevelFourPaddingLeft" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($level4PaddingLeft) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildEffectsGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-magic me-2" style="color: #FF8600;"></i>';
$html .= ' Efectos Visuales';
$html .= ' </h5>';
$html .= ' <div class="row g-2 mb-3">';
// border_radius
$borderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_radius', '8px');
$html .= ' <div class="col-6">';
$html .= ' <label for="tocBorderRadius" class="form-label small mb-1 fw-semibold">Radio borde</label>';
$html .= ' <input type="text" id="tocBorderRadius" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($borderRadius) . '">';
$html .= ' </div>';
// border_width
$borderWidth = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_width', '1px');
$html .= ' <div class="col-6">';
$html .= ' <label for="tocBorderWidth" class="form-label small mb-1 fw-semibold">Grosor borde</label>';
$html .= ' <input type="text" id="tocBorderWidth" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($borderWidth) . '">';
$html .= ' </div>';
$html .= ' </div>';
// box_shadow
$boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', '0 2px 8px rgba(0, 0, 0, 0.08)');
$html .= ' <div class="mb-3">';
$html .= ' <label for="tocBoxShadow" class="form-label small mb-1 fw-semibold">Sombra</label>';
$html .= ' <input type="text" id="tocBoxShadow" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($boxShadow) . '">';
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-3">';
// link_border_radius
$linkBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'link_border_radius', '4px');
$html .= ' <div class="col-6">';
$html .= ' <label for="tocLinkBorderRadius" class="form-label small mb-1 fw-semibold">Radio enlaces</label>';
$html .= ' <input type="text" id="tocLinkBorderRadius" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($linkBorderRadius) . '">';
$html .= ' </div>';
// active_border_left_width
$activeBorderLeftWidth = $this->renderer->getFieldValue($componentId, 'visual_effects', 'active_border_left_width', '3px');
$html .= ' <div class="col-6">';
$html .= ' <label for="tocActiveBorderLeftWidth" class="form-label small mb-1 fw-semibold">Borde activo</label>';
$html .= ' <input type="text" id="tocActiveBorderLeftWidth" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($activeBorderLeftWidth) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' <div class="row g-2 mb-0">';
// transition_duration
$transitionDuration = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s');
$html .= ' <div class="col-6">';
$html .= ' <label for="tocTransitionDuration" class="form-label small mb-1 fw-semibold">Transicion</label>';
$html .= ' <input type="text" id="tocTransitionDuration" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($transitionDuration) . '">';
$html .= ' </div>';
// scrollbar_border_radius
$scrollbarBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'scrollbar_border_radius', '3px');
$html .= ' <div class="col-6">';
$html .= ' <label for="tocScrollbarBorderRadius" class="form-label small mb-1 fw-semibold">Radio scrollbar</label>';
$html .= ' <input type="text" id="tocScrollbarBorderRadius" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($scrollbarBorderRadius) . '">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildSwitch(string $id, string $label, string $icon, bool $checked): string
{
$html = ' <div class="mb-2">';
$html .= ' <div class="form-check form-switch">';
$html .= sprintf(
' <input class="form-check-input" type="checkbox" id="%s" %s>',
esc_attr($id),
$checked ? 'checked' : ''
);
$html .= sprintf(
' <label class="form-check-label small" for="%s">',
esc_attr($id)
);
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
$html .= sprintf(' <strong>%s</strong>', esc_html($label));
$html .= ' </label>';
$html .= ' </div>';
$html .= ' </div>';
return $html;
}
private function buildColorPicker(string $id, string $label, string $value): string
{
$html = ' <div class="col-6">';
$html .= sprintf(
' <label class="form-label small fw-semibold">%s</label>',
esc_html($label)
);
$html .= ' <div class="input-group input-group-sm">';
$html .= sprintf(
' <input type="color" class="form-control form-control-color" id="%s" value="%s">',
esc_attr($id),
esc_attr($value)
);
$html .= sprintf(
' <span class="input-group-text" id="%sValue">%s</span>',
esc_attr($id),
esc_html(strtoupper($value))
);
$html .= ' </div>';
$html .= ' </div>';
return $html;
}
}

View File

@@ -0,0 +1,308 @@
<?php
declare(strict_types=1);
namespace ROITheme\Admin\TopNotificationBar\Infrastructure\Ui;
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
final class TopNotificationBarFormBuilder
{
public function __construct(
private AdminDashboardRenderer $renderer
) {}
public function buildForm(string $componentId): string
{
$html = '';
// Header
$html .= $this->buildHeader($componentId);
// Layout 2 columnas
$html .= '<div class="row g-3">';
$html .= ' <div class="col-lg-6">';
$html .= $this->buildVisibilityGroup($componentId);
$html .= $this->buildContentGroup($componentId);
$html .= ' </div>';
$html .= ' <div class="col-lg-6">';
$html .= $this->buildColorsGroup($componentId);
$html .= $this->buildTypographyAndSpacingGroup($componentId);
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildHeader(string $componentId): string
{
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
$html .= ' <div>';
$html .= ' <h3 class="h4 mb-1 fw-bold">';
$html .= ' <i class="bi bi-megaphone-fill me-2" style="color: #FF8600;"></i>';
$html .= ' Configuración de TopBar';
$html .= ' </h3>';
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
$html .= ' Personaliza la barra de notificación superior del sitio';
$html .= ' </p>';
$html .= ' </div>';
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="top-notification-bar">';
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
$html .= ' Restaurar valores por defecto';
$html .= ' </button>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildVisibilityGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
$html .= ' Activación y Visibilidad';
$html .= ' </h5>';
// Switch: Enabled
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
$html .= ' <div class="mb-2">';
$html .= ' <div class="form-check form-switch">';
$html .= ' <input class="form-check-input" type="checkbox" id="topBarEnabled" ';
$html .= checked($enabled, true, false) . '>';
$html .= ' <label class="form-check-label small" for="topBarEnabled" style="color: #495057;">';
$html .= ' <i class="bi bi-power me-1" style="color: #FF8600;"></i>';
$html .= ' <strong>Activar TopBar</strong>';
$html .= ' </label>';
$html .= ' </div>';
$html .= ' </div>';
// Switch: Show on Mobile
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
$html .= ' <div class="mb-2">';
$html .= ' <div class="form-check form-switch">';
$html .= ' <input class="form-check-input" type="checkbox" id="topBarShowOnMobile" ';
$html .= checked($showOnMobile, true, false) . '>';
$html .= ' <label class="form-check-label small" for="topBarShowOnMobile" style="color: #495057;">';
$html .= ' <i class="bi bi-phone me-1" style="color: #FF8600;"></i>';
$html .= ' <strong>Mostrar en Mobile</strong> <span class="text-muted">(&lt;768px)</span>';
$html .= ' </label>';
$html .= ' </div>';
$html .= ' </div>';
// Switch: Show on Desktop
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
$html .= ' <div class="mb-2">';
$html .= ' <div class="form-check form-switch">';
$html .= ' <input class="form-check-input" type="checkbox" id="topBarShowOnDesktop" ';
$html .= checked($showOnDesktop, true, false) . '>';
$html .= ' <label class="form-check-label small" for="topBarShowOnDesktop" style="color: #495057;">';
$html .= ' <i class="bi bi-display me-1" style="color: #FF8600;"></i>';
$html .= ' <strong>Mostrar en Desktop</strong> <span class="text-muted">(≥768px)</span>';
$html .= ' </label>';
$html .= ' </div>';
$html .= ' </div>';
// Select: Show on Pages
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all');
$html .= ' <div class="mb-0 mt-3">';
$html .= ' <label for="topBarShowOnPages" class="form-label small mb-1 fw-semibold" style="color: #495057;">';
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
$html .= ' Mostrar en';
$html .= ' </label>';
$html .= ' <select id="topBarShowOnPages" class="form-select form-select-sm">';
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las páginas</option>';
$html .= ' <option value="home" ' . selected($showOnPages, 'home', false) . '>Solo página de inicio</option>';
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo páginas</option>';
$html .= ' </select>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildContentGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-chat-text me-2" style="color: #FF8600;"></i>';
$html .= ' Contenido';
$html .= ' </h5>';
// icon_class + label_text (row)
$html .= ' <div class="row g-2 mb-2">';
$html .= ' <div class="col-6">';
$html .= ' <label for="topBarIconClass" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-star-fill me-1" style="color: #FF8600;"></i>';
$html .= ' Clase del ícono';
$html .= ' </label>';
$iconClass = $this->renderer->getFieldValue($componentId, 'content', 'icon_class', 'bi-megaphone-fill');
$html .= ' <input type="text" id="topBarIconClass" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($iconClass) . '" placeholder="bi-...">';
$html .= ' </div>';
$html .= ' <div class="col-6">';
$html .= ' <label for="topBarLabelText" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-tag me-1" style="color: #FF8600;"></i>';
$html .= ' Etiqueta';
$html .= ' </label>';
$labelText = $this->renderer->getFieldValue($componentId, 'content', 'label_text', 'Nuevo:');
$html .= ' <input type="text" id="topBarLabelText" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($labelText) . '" maxlength="30">';
$html .= ' </div>';
$html .= ' </div>';
// message_text (textarea)
$messageText = $this->renderer->getFieldValue($componentId, 'content', 'message_text',
'Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.');
$html .= ' <div class="mb-2">';
$html .= ' <label for="topBarMessageText" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-chat-dots me-1" style="color: #FF8600;"></i>';
$html .= ' Mensaje';
$html .= ' </label>';
$html .= ' <textarea id="topBarMessageText" class="form-control form-control-sm" rows="3" maxlength="200">';
$html .= esc_textarea($messageText);
$html .= ' </textarea>';
$html .= ' <small class="text-muted">Máximo 200 caracteres</small>';
$html .= ' </div>';
// link_text + link_url (row)
$html .= ' <div class="row g-2 mb-0">';
$html .= ' <div class="col-6">';
$html .= ' <label for="topBarLinkText" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-link-45deg me-1" style="color: #FF8600;"></i>';
$html .= ' Texto del enlace';
$html .= ' </label>';
$linkText = $this->renderer->getFieldValue($componentId, 'content', 'link_text', 'Ver Catálogo');
$html .= ' <input type="text" id="topBarLinkText" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($linkText) . '" maxlength="50">';
$html .= ' </div>';
$html .= ' <div class="col-6">';
$html .= ' <label for="topBarLinkUrl" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-box-arrow-up-right me-1" style="color: #FF8600;"></i>';
$html .= ' URL';
$html .= ' </label>';
$linkUrl = $this->renderer->getFieldValue($componentId, 'content', 'link_url', '#');
$html .= ' <input type="url" id="topBarLinkUrl" class="form-control form-control-sm" ';
$html .= ' value="' . esc_url($linkUrl) . '" placeholder="https://...">';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildColorsGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
$html .= ' Colores';
$html .= ' </h5>';
// Grid 2x3 de color pickers
$html .= ' <div class="row g-2 mb-2">';
// Background Color
$bgColor = $this->renderer->getFieldValue($componentId, 'colors', 'background_color', '#0E2337');
$html .= $this->buildColorPicker('topBarBackgroundColor', 'Color de fondo', 'paint-bucket', $bgColor);
// Text Color
$textColor = $this->renderer->getFieldValue($componentId, 'colors', 'text_color', '#FFFFFF');
$html .= $this->buildColorPicker('topBarTextColor', 'Color de texto', 'fonts', $textColor);
// Label Color
$labelColor = $this->renderer->getFieldValue($componentId, 'colors', 'label_color', '#FF8600');
$html .= $this->buildColorPicker('topBarLabelColor', 'Color etiqueta', 'tag-fill', $labelColor);
// Icon Color
$iconColor = $this->renderer->getFieldValue($componentId, 'colors', 'icon_color', '#FF8600');
$html .= $this->buildColorPicker('topBarIconColor', 'Color ícono', 'star', $iconColor);
$html .= ' </div>';
// Row 2 de color pickers
$html .= ' <div class="row g-2 mb-0">';
// Link Color
$linkColor = $this->renderer->getFieldValue($componentId, 'colors', 'link_color', '#FFFFFF');
$html .= $this->buildColorPicker('topBarLinkColor', 'Color enlace', 'link', $linkColor);
// Link Hover Color
$linkHoverColor = $this->renderer->getFieldValue($componentId, 'colors', 'link_hover_color', '#FF8600');
$html .= $this->buildColorPicker('topBarLinkHoverColor', 'Color enlace (hover)', 'hand-index', $linkHoverColor);
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildTypographyAndSpacingGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-arrows-fullscreen me-2" style="color: #FF8600;"></i>';
$html .= ' Tipografía y Espaciado';
$html .= ' </h5>';
$html .= ' <div class="row g-2 mb-0">';
// Font Size
$html .= ' <div class="col-6">';
$html .= ' <label for="topBarFontSize" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-type me-1" style="color: #FF8600;"></i>';
$html .= ' Tamaño de fuente';
$html .= ' </label>';
$fontSize = $this->renderer->getFieldValue($componentId, 'spacing', 'font_size', '0.9rem');
$html .= ' <input type="text" id="topBarFontSize" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($fontSize) . '">';
$html .= ' <small class="text-muted">Ej: 0.9rem, 14px</small>';
$html .= ' </div>';
// Padding
$html .= ' <div class="col-6">';
$html .= ' <label for="topBarPadding" class="form-label small mb-1 fw-semibold">';
$html .= ' <i class="bi bi-bounding-box me-1" style="color: #FF8600;"></i>';
$html .= ' Padding vertical';
$html .= ' </label>';
$padding = $this->renderer->getFieldValue($componentId, 'spacing', 'padding', '0.5rem 0');
$html .= ' <input type="text" id="topBarPadding" class="form-control form-control-sm" ';
$html .= ' value="' . esc_attr($padding) . '">';
$html .= ' <small class="text-muted">Ej: 0.5rem 0</small>';
$html .= ' </div>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildColorPicker(string $id, string $label, string $icon, string $value): string
{
$html = ' <div class="col-6">';
$html .= ' <label for="' . $id . '" class="form-label small mb-1 fw-semibold" style="color: #495057;">';
$html .= ' <i class="bi bi-' . $icon . ' me-1" style="color: #FF8600;"></i>';
$html .= ' ' . $label;
$html .= ' </label>';
$html .= ' <input type="color" id="' . $id . '" class="form-control form-control-color w-100" ';
$html .= ' value="' . esc_attr($value) . '" title="' . esc_attr($label) . '">';
$html .= ' <small class="text-muted d-block mt-1" id="' . $id . 'Value">' . esc_html(strtoupper($value)) . '</small>';
$html .= ' </div>';
return $html;
}
}

View File

@@ -0,0 +1,283 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TopBar - Preview de Diseño</title>
<!-- Bootstrap 5 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Poppins', sans-serif;
background-color: #f0f0f1;
padding: 20px;
}
</style>
</head>
<body>
<!-- ============================================================
TAB: TOP NOTIFICATION BAR CONFIGURATION
============================================================ -->
<div class="tab-pane fade show active" id="topBarTab" role="tabpanel">
<!-- ========================================
PATRÓN 1: HEADER CON GRADIENTE
======================================== -->
<div class="rounded p-4 mb-4 shadow text-white" style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
<div>
<h3 class="h4 mb-1 fw-bold">
<i class="bi bi-megaphone-fill me-2" style="color: #FF8600;"></i>
Configuración de TopBar
</h3>
<p class="mb-0 small" style="opacity: 0.85;">
Personaliza la barra de notificación superior del sitio
</p>
</div>
<button type="button" class="btn btn-sm btn-outline-light" id="resetTopBarDefaults">
<i class="bi bi-arrow-counterclockwise me-1"></i>
Restaurar valores por defecto
</button>
</div>
</div>
<!-- ========================================
PATRÓN 2: LAYOUT 2 COLUMNAS
======================================== -->
<div class="row g-3">
<div class="col-lg-6">
<!-- ========================================
GRUPO 1: ACTIVACIÓN Y VISIBILIDAD (OBLIGATORIO)
PATRÓN 3: CARD CON BORDER-LEFT NAVY
======================================== -->
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
<div class="card-body">
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
<i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>
Activación y Visibilidad
</h5>
<!-- ⚠️ PATRÓN 4: SWITCHES VERTICALES CON ICONOS (3 OBLIGATORIOS) -->
<!-- Switch 1: Enabled (OBLIGATORIO) -->
<div class="mb-2">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="topBarEnabled" checked>
<label class="form-check-label small" for="topBarEnabled" style="color: #495057;">
<i class="bi bi-power me-1" style="color: #FF8600;"></i>
<strong>Activar TopBar</strong>
</label>
</div>
</div>
<!-- Switch 2: Show on Mobile (OBLIGATORIO) -->
<div class="mb-2">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="topBarShowOnMobile" checked>
<label class="form-check-label small" for="topBarShowOnMobile" style="color: #495057;">
<i class="bi bi-phone me-1" style="color: #FF8600;"></i>
<strong>Mostrar en Mobile</strong> <span class="text-muted">(&lt;768px)</span>
</label>
</div>
</div>
<!-- Switch 3: Show on Desktop (OBLIGATORIO) -->
<div class="mb-2">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="topBarShowOnDesktop" checked>
<label class="form-check-label small" for="topBarShowOnDesktop" style="color: #495057;">
<i class="bi bi-display me-1" style="color: #FF8600;"></i>
<strong>Mostrar en Desktop</strong> <span class="text-muted">(≥768px)</span>
</label>
</div>
</div>
<!-- Campo adicional del schema: show_on_pages (select) -->
<div class="mb-0 mt-3">
<label for="topBarShowOnPages" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>
Mostrar en
</label>
<select id="topBarShowOnPages" class="form-select form-select-sm">
<option value="all" selected>Todas las páginas</option>
<option value="home">Solo página de inicio</option>
<option value="posts">Solo posts individuales</option>
<option value="pages">Solo páginas</option>
</select>
</div>
</div>
</div>
<!-- ========================================
GRUPO 2: CONTENIDO
======================================== -->
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
<div class="card-body">
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
<i class="bi bi-chat-text me-2" style="color: #FF8600;"></i>
Contenido
</h5>
<!-- icon_class + label_text (compactados) -->
<div class="row g-2 mb-2">
<div class="col-6">
<label for="topBarIconClass" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-star-fill me-1" style="color: #FF8600;"></i>
Clase del ícono
</label>
<input type="text" id="topBarIconClass" class="form-control form-control-sm" value="bi-megaphone-fill" placeholder="bi-...">
</div>
<div class="col-6">
<label for="topBarLabelText" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-tag me-1" style="color: #FF8600;"></i>
Etiqueta
</label>
<input type="text" id="topBarLabelText" class="form-control form-control-sm" value="Nuevo:" maxlength="30">
</div>
</div>
<!-- message_text (textarea full width) -->
<div class="mb-2">
<label for="topBarMessageText" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-chat-dots me-1" style="color: #FF8600;"></i>
Mensaje
</label>
<textarea id="topBarMessageText" class="form-control form-control-sm" rows="3" maxlength="200">Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.</textarea>
<small class="text-muted">Máximo 200 caracteres</small>
</div>
<!-- link_text + link_url (compactados) -->
<div class="row g-2 mb-0">
<div class="col-6">
<label for="topBarLinkText" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-link-45deg me-1" style="color: #FF8600;"></i>
Texto del enlace
</label>
<input type="text" id="topBarLinkText" class="form-control form-control-sm" value="Ver Catálogo" maxlength="50">
</div>
<div class="col-6">
<label for="topBarLinkUrl" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-box-arrow-up-right me-1" style="color: #FF8600;"></i>
URL
</label>
<input type="url" id="topBarLinkUrl" class="form-control form-control-sm" value="#" placeholder="https://...">
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-6">
<!-- ========================================
GRUPO 3: ESTILOS - COLORES
======================================== -->
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
<div class="card-body">
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
<i class="bi bi-palette me-2" style="color: #FF8600;"></i>
Estilos - Colores
</h5>
<!-- PATRÓN 5: COLOR PICKERS EN GRID 2X2 -->
<div class="row g-2 mb-2">
<div class="col-6">
<label for="topBarBackgroundColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-paint-bucket me-1" style="color: #FF8600;"></i>
Color de fondo
</label>
<input type="color" id="topBarBackgroundColor" class="form-control form-control-color w-100" value="#0E2337" title="Color de fondo">
<small class="text-muted d-block mt-1" id="topBarBackgroundColorValue">#0E2337</small>
</div>
<div class="col-6">
<label for="topBarTextColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-fonts me-1" style="color: #FF8600;"></i>
Color de texto
</label>
<input type="color" id="topBarTextColor" class="form-control form-control-color w-100" value="#FFFFFF" title="Color de texto">
<small class="text-muted d-block mt-1" id="topBarTextColorValue">#FFFFFF</small>
</div>
<div class="col-6">
<label for="topBarLabelColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-tag-fill me-1" style="color: #FF8600;"></i>
Color etiqueta
</label>
<input type="color" id="topBarLabelColor" class="form-control form-control-color w-100" value="#FF8600" title="Color etiqueta">
<small class="text-muted d-block mt-1" id="topBarLabelColorValue">#FF8600</small>
</div>
<div class="col-6">
<label for="topBarIconColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-star me-1" style="color: #FF8600;"></i>
Color ícono
</label>
<input type="color" id="topBarIconColor" class="form-control form-control-color w-100" value="#FF8600" title="Color ícono">
<small class="text-muted d-block mt-1" id="topBarIconColorValue">#FF8600</small>
</div>
</div>
<div class="row g-2 mb-0">
<div class="col-6">
<label for="topBarLinkColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-link me-1" style="color: #FF8600;"></i>
Color enlace
</label>
<input type="color" id="topBarLinkColor" class="form-control form-control-color w-100" value="#FFFFFF" title="Color enlace">
<small class="text-muted d-block mt-1" id="topBarLinkColorValue">#FFFFFF</small>
</div>
<div class="col-6">
<label for="topBarLinkHoverColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-hand-index me-1" style="color: #FF8600;"></i>
Color enlace (hover)
</label>
<input type="color" id="topBarLinkHoverColor" class="form-control form-control-color w-100" value="#FF8600" title="Color enlace hover">
<small class="text-muted d-block mt-1" id="topBarLinkHoverColorValue">#FF8600</small>
</div>
</div>
</div>
</div>
<!-- ========================================
GRUPO 4: ESTILOS - TAMAÑOS
======================================== -->
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
<div class="card-body">
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
<i class="bi bi-arrows-fullscreen me-2" style="color: #FF8600;"></i>
Estilos - Tamaños
</h5>
<div class="row g-2 mb-0">
<div class="col-6">
<label for="topBarFontSize" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-type me-1" style="color: #FF8600;"></i>
Tamaño de fuente
</label>
<input type="text" id="topBarFontSize" class="form-control form-control-sm" value="0.9rem">
<small class="text-muted">Ej: 0.9rem, 14px</small>
</div>
<div class="col-6">
<label for="topBarPadding" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-bounding-box me-1" style="color: #FF8600;"></i>
Padding vertical
</label>
<input type="text" id="topBarPadding" class="form-control form-control-sm" value="0.5rem 0">
<small class="text-muted">Ej: 0.5rem 0</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div><!-- /tab-pane -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -0,0 +1,303 @@
<?php
declare(strict_types=1);
namespace ROITheme\Public\ContactForm\Infrastructure\Api\WordPress;
use ROITheme\Shared\Domain\Contracts\ComponentSettingsRepositoryInterface;
/**
* ContactFormAjaxHandler - Procesa envios del formulario de contacto
*
* RESPONSABILIDAD: Recibir datos del formulario y enviarlos al webhook configurado
*
* SEGURIDAD:
* - Verifica nonce
* - Webhook URL NUNCA se expone al cliente
* - Webhook URL se obtiene de BD server-side
* - Rate limiting basico
* - Sanitizacion de inputs
*
* @package ROITheme\Public\ContactForm\Infrastructure\Api\WordPress
*/
final class ContactFormAjaxHandler
{
private const NONCE_ACTION = 'roi_contact_form_nonce';
private const COMPONENT_NAME = 'contact-form';
public function __construct(
private ComponentSettingsRepositoryInterface $settingsRepository
) {}
/**
* Registrar hooks AJAX
* Usa wp_ajax_nopriv para usuarios no logueados
*/
public function register(): void
{
add_action('wp_ajax_roi_contact_form_submit', [$this, 'handleSubmit']);
add_action('wp_ajax_nopriv_roi_contact_form_submit', [$this, 'handleSubmit']);
}
/**
* Procesar envio del formulario
*/
public function handleSubmit(): void
{
// 1. Verificar nonce
$nonce = sanitize_text_field($_POST['nonce'] ?? '');
if (!wp_verify_nonce($nonce, self::NONCE_ACTION)) {
wp_send_json_error([
'message' => __('Error de seguridad. Por favor recarga la pagina.', 'roi-theme')
], 403);
return;
}
// 2. Rate limiting basico (1 envio por IP cada 30 segundos)
if (!$this->checkRateLimit()) {
wp_send_json_error([
'message' => __('Por favor espera un momento antes de enviar otro mensaje.', 'roi-theme')
], 429);
return;
}
// 3. Sanitizar y validar inputs
$formData = $this->sanitizeFormData($_POST);
$validation = $this->validateFormData($formData);
if (!$validation['valid']) {
wp_send_json_error([
'message' => $validation['message'],
'errors' => $validation['errors']
], 422);
return;
}
// 4. Obtener configuracion del componente (incluye webhook URL)
$settings = $this->settingsRepository->getComponentSettings(self::COMPONENT_NAME);
if (empty($settings)) {
wp_send_json_error([
'message' => __('Error de configuracion. Contacta al administrador.', 'roi-theme')
], 500);
return;
}
$integration = $settings['integration'] ?? [];
$webhookUrl = $integration['webhook_url'] ?? '';
$webhookMethod = $integration['webhook_method'] ?? 'POST';
$includePageUrl = $this->toBool($integration['include_page_url'] ?? true);
$includeTimestamp = $this->toBool($integration['include_timestamp'] ?? true);
if (empty($webhookUrl)) {
// Si no hay webhook configurado, simular exito para UX
// pero loguear warning para admin
error_log('ROI Theme Contact Form: No webhook URL configured');
wp_send_json_success([
'message' => $this->getSuccessMessage($settings)
]);
return;
}
// 5. Preparar payload para webhook
$payload = $this->preparePayload($formData, $includePageUrl, $includeTimestamp);
// 6. Enviar a webhook
$result = $this->sendToWebhook($webhookUrl, $webhookMethod, $payload);
if ($result['success']) {
wp_send_json_success([
'message' => $this->getSuccessMessage($settings)
]);
} else {
error_log('ROI Theme Contact Form webhook error: ' . $result['error']);
wp_send_json_error([
'message' => $this->getErrorMessage($settings)
], 500);
}
}
/**
* Sanitizar datos del formulario
*/
private function sanitizeFormData(array $post): array
{
return [
'fullName' => sanitize_text_field($post['fullName'] ?? ''),
'company' => sanitize_text_field($post['company'] ?? ''),
'whatsapp' => sanitize_text_field($post['whatsapp'] ?? ''),
'email' => sanitize_email($post['email'] ?? ''),
'message' => sanitize_textarea_field($post['message'] ?? ''),
];
}
/**
* Validar datos del formulario
*/
private function validateFormData(array $data): array
{
$errors = [];
// Nombre requerido
if (empty($data['fullName'])) {
$errors['fullName'] = __('El nombre es obligatorio', 'roi-theme');
}
// WhatsApp requerido
if (empty($data['whatsapp'])) {
$errors['whatsapp'] = __('El WhatsApp es obligatorio', 'roi-theme');
}
// Email requerido y valido
if (empty($data['email'])) {
$errors['email'] = __('El email es obligatorio', 'roi-theme');
} elseif (!is_email($data['email'])) {
$errors['email'] = __('Por favor ingresa un email valido', 'roi-theme');
}
if (!empty($errors)) {
return [
'valid' => false,
'message' => __('Por favor corrige los errores del formulario', 'roi-theme'),
'errors' => $errors
];
}
return ['valid' => true, 'message' => '', 'errors' => []];
}
/**
* Preparar payload para webhook
*/
private function preparePayload(array $formData, bool $includePageUrl, bool $includeTimestamp): array
{
$payload = [
'fullName' => $formData['fullName'],
'company' => $formData['company'],
'whatsapp' => $formData['whatsapp'],
'email' => $formData['email'],
'message' => $formData['message'],
];
if ($includePageUrl) {
$payload['pageUrl'] = sanitize_url($_POST['pageUrl'] ?? '');
$payload['pageTitle'] = sanitize_text_field($_POST['pageTitle'] ?? '');
}
if ($includeTimestamp) {
$payload['timestamp'] = current_time('c');
$payload['timezone'] = wp_timezone_string();
}
// Metadata adicional util para el webhook
$payload['source'] = 'contact-form';
$payload['siteName'] = get_bloginfo('name');
$payload['siteUrl'] = home_url();
return $payload;
}
/**
* Enviar datos al webhook
*/
private function sendToWebhook(string $url, string $method, array $payload): array
{
$args = [
'method' => strtoupper($method),
'timeout' => 30,
'redirection' => 5,
'httpversion' => '1.1',
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
];
if ($method === 'POST') {
$args['body'] = wp_json_encode($payload);
} else {
$url = add_query_arg($payload, $url);
}
$response = wp_remote_request($url, $args);
if (is_wp_error($response)) {
return [
'success' => false,
'error' => $response->get_error_message()
];
}
$statusCode = wp_remote_retrieve_response_code($response);
// Considerar 2xx como exito
if ($statusCode >= 200 && $statusCode < 300) {
return ['success' => true, 'error' => ''];
}
return [
'success' => false,
'error' => sprintf('HTTP %d: %s', $statusCode, wp_remote_retrieve_response_message($response))
];
}
/**
* Rate limiting basico por IP
*/
private function checkRateLimit(): bool
{
$ip = $this->getClientIP();
$transientKey = 'roi_contact_form_' . md5($ip);
$lastSubmit = get_transient($transientKey);
if ($lastSubmit !== false) {
return false;
}
set_transient($transientKey, time(), 30);
return true;
}
/**
* Obtener IP del cliente
*/
private function getClientIP(): string
{
$ip = '';
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
$ip = sanitize_text_field($_SERVER['HTTP_CLIENT_IP']);
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip = sanitize_text_field(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0]);
} elseif (!empty($_SERVER['REMOTE_ADDR'])) {
$ip = sanitize_text_field($_SERVER['REMOTE_ADDR']);
}
return $ip;
}
/**
* Obtener mensaje de exito desde configuracion
*/
private function getSuccessMessage(array $data): string
{
$messages = $data['messages'] ?? [];
return $messages['success_message'] ?? __('¡Gracias por contactarnos! Te responderemos pronto.', 'roi-theme');
}
/**
* Obtener mensaje de error desde configuracion
*/
private function getErrorMessage(array $data): string
{
$messages = $data['messages'] ?? [];
return $messages['error_message'] ?? __('Hubo un error al enviar el mensaje. Por favor intenta de nuevo.', 'roi-theme');
}
/**
* Convertir valor a boolean
*/
private function toBool($value): bool
{
return $value === true || $value === '1' || $value === 1;
}
}

View File

@@ -0,0 +1,461 @@
<?php
declare(strict_types=1);
namespace ROITheme\Public\ContactForm\Infrastructure\Ui;
use ROITheme\Shared\Domain\Contracts\RendererInterface;
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
use ROITheme\Shared\Domain\Entities\Component;
/**
* ContactFormRenderer - Renderiza formulario de contacto con webhook
*
* RESPONSABILIDAD: Generar HTML y CSS del formulario de contacto
*
* CARACTERISTICAS:
* - Formulario responsive Bootstrap 5
* - Envio a webhook configurable (no expuesto en frontend)
* - Info de contacto configurable
* - Mensajes de exito/error personalizables
*
* @package ROITheme\Public\ContactForm\Infrastructure\Ui
*/
final class ContactFormRenderer implements RendererInterface
{
public function __construct(
private CSSGeneratorInterface $cssGenerator
) {}
public function render(Component $component): string
{
$data = $component->getData();
if (!$this->isEnabled($data)) {
return '';
}
if (!$this->shouldShowOnCurrentPage($data)) {
return '';
}
$visibilityClass = $this->getVisibilityClass($data);
if ($visibilityClass === null) {
return '';
}
$css = $this->generateCSS($data);
$html = $this->buildHTML($data, $visibilityClass);
$js = $this->buildJS($data);
return sprintf("<style>%s</style>\n%s\n<script>%s</script>", $css, $html, $js);
}
public function supports(string $componentType): bool
{
return $componentType === 'contact-form';
}
private function isEnabled(array $data): bool
{
$value = $data['visibility']['is_enabled'] ?? false;
return $value === true || $value === '1' || $value === 1;
}
private function shouldShowOnCurrentPage(array $data): bool
{
$showOn = $data['visibility']['show_on_pages'] ?? 'all';
switch ($showOn) {
case 'all':
return true;
case 'posts':
return is_single();
case 'pages':
return is_page();
default:
return true;
}
}
private function getVisibilityClass(array $data): ?string
{
$showDesktop = $data['visibility']['show_on_desktop'] ?? true;
$showDesktop = $showDesktop === true || $showDesktop === '1' || $showDesktop === 1;
$showMobile = $data['visibility']['show_on_mobile'] ?? true;
$showMobile = $showMobile === true || $showMobile === '1' || $showMobile === 1;
if (!$showDesktop && !$showMobile) {
return null;
}
if (!$showDesktop && $showMobile) {
return 'd-lg-none';
}
if ($showDesktop && !$showMobile) {
return 'd-none d-lg-block';
}
return '';
}
private function generateCSS(array $data): string
{
$colors = $data['colors'] ?? [];
$spacing = $data['spacing'] ?? [];
$effects = $data['visual_effects'] ?? [];
$cssRules = [];
// Section background
$sectionBgColor = $colors['section_bg_color'] ?? 'rgba(108, 117, 125, 0.25)';
$sectionPaddingY = $spacing['section_padding_y'] ?? '3rem';
$sectionMarginTop = $spacing['section_margin_top'] ?? '3rem';
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section', [
'background-color' => $sectionBgColor,
'padding-top' => $sectionPaddingY,
'padding-bottom' => $sectionPaddingY,
'margin-top' => $sectionMarginTop,
]);
// Title
$titleColor = $colors['title_color'] ?? '#212529';
$titleMarginBottom = $spacing['title_margin_bottom'] ?? '0.75rem';
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .contact-title', [
'color' => $titleColor,
'margin-bottom' => $titleMarginBottom,
]);
// Description
$descColor = $colors['description_color'] ?? '#212529';
$descMarginBottom = $spacing['description_margin_bottom'] ?? '1.5rem';
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .contact-description', [
'color' => $descColor,
'margin-bottom' => $descMarginBottom,
]);
// Icons
$iconColor = $colors['icon_color'] ?? '#FF8600';
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .contact-icon', [
'color' => $iconColor,
]);
// Info labels and values
$infoLabelColor = $colors['info_label_color'] ?? '#212529';
$infoValueColor = $colors['info_value_color'] ?? '#6c757d';
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .info-label', [
'color' => $infoLabelColor,
]);
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .info-value', [
'color' => $infoValueColor,
]);
// Form inputs
$inputBorderColor = $colors['input_border_color'] ?? '#dee2e6';
$inputFocusBorder = $colors['input_focus_border'] ?? '#FF8600';
$inputBorderRadius = $effects['input_border_radius'] ?? '6px';
$transitionDuration = $effects['transition_duration'] ?? '0.3s';
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .form-control', [
'border-color' => $inputBorderColor,
'border-radius' => $inputBorderRadius,
'transition' => "all {$transitionDuration} ease",
]);
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .form-control:focus', [
'border-color' => $inputFocusBorder,
'box-shadow' => "0 0 0 0.2rem rgba(255, 134, 0, 0.25)",
'outline' => 'none',
]);
// Submit button
$buttonBgColor = $colors['button_bg_color'] ?? '#FF8600';
$buttonTextColor = $colors['button_text_color'] ?? '#ffffff';
$buttonHoverBg = $colors['button_hover_bg'] ?? '#e67a00';
$buttonBorderRadius = $effects['button_border_radius'] ?? '6px';
$buttonPadding = $effects['button_padding'] ?? '0.75rem 2rem';
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .btn-contact-submit', [
'background-color' => $buttonBgColor,
'color' => $buttonTextColor,
'font-weight' => '600',
'padding' => $buttonPadding,
'border' => 'none',
'border-radius' => $buttonBorderRadius,
'transition' => "all {$transitionDuration} ease",
]);
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .btn-contact-submit:hover', [
'background-color' => $buttonHoverBg,
'color' => $buttonTextColor,
]);
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .btn-contact-submit:disabled', [
'opacity' => '0.7',
'cursor' => 'not-allowed',
]);
// Success/Error messages
$successBgColor = $colors['success_bg_color'] ?? '#d1e7dd';
$successTextColor = $colors['success_text_color'] ?? '#0f5132';
$errorBgColor = $colors['error_bg_color'] ?? '#f8d7da';
$errorTextColor = $colors['error_text_color'] ?? '#842029';
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .alert-success', [
'background-color' => $successBgColor,
'color' => $successTextColor,
'border-color' => $successBgColor,
]);
$cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .alert-danger', [
'background-color' => $errorBgColor,
'color' => $errorTextColor,
'border-color' => $errorBgColor,
]);
return implode("\n", $cssRules);
}
private function buildHTML(array $data, string $visibilityClass): string
{
$content = $data['content'] ?? [];
$contactInfo = $data['contact_info'] ?? [];
$formLabels = $data['form_labels'] ?? [];
$effects = $data['visual_effects'] ?? [];
// Content
$sectionTitle = $content['section_title'] ?? '¿Tienes alguna pregunta?';
$sectionDesc = $content['section_description'] ?? 'Completa el formulario y nuestro equipo te responderá en menos de 24 horas.';
$submitText = $content['submit_button_text'] ?? 'Enviar Mensaje';
$submitIcon = $content['submit_button_icon'] ?? 'bi-send-fill';
// Contact info
$showContactInfo = $contactInfo['show_contact_info'] ?? true;
$showContactInfo = $showContactInfo === true || $showContactInfo === '1' || $showContactInfo === 1;
// Form labels/placeholders
$fullnamePlaceholder = $formLabels['fullname_placeholder'] ?? 'Nombre completo *';
$companyPlaceholder = $formLabels['company_placeholder'] ?? 'Empresa';
$whatsappPlaceholder = $formLabels['whatsapp_placeholder'] ?? 'WhatsApp *';
$emailPlaceholder = $formLabels['email_placeholder'] ?? 'Correo electrónico *';
$messagePlaceholder = $formLabels['message_placeholder'] ?? '¿En qué podemos ayudarte?';
$textareaRows = $effects['textarea_rows'] ?? '4';
// Container class
$containerClass = 'roi-contact-form-section';
if (!empty($visibilityClass)) {
$containerClass .= ' ' . $visibilityClass;
}
// Nonce for AJAX security
$nonce = wp_create_nonce('roi_contact_form_nonce');
$html = sprintf('<section class="%s">', esc_attr($containerClass));
$html .= '<div class="container">';
$html .= '<div class="row justify-content-center">';
$html .= '<div class="col-lg-10">';
$html .= '<div class="row">';
// Left column - Contact info
$html .= '<div class="col-lg-5 mb-4 mb-lg-0">';
$html .= sprintf('<h2 class="h3 contact-title">%s</h2>', esc_html($sectionTitle));
$html .= sprintf('<p class="contact-description">%s</p>', esc_html($sectionDesc));
if ($showContactInfo) {
$html .= $this->buildContactInfoHTML($contactInfo);
}
$html .= '</div>';
// Right column - Form
$html .= '<div class="col-lg-7">';
$html .= sprintf('<form id="roiContactForm" data-nonce="%s">', esc_attr($nonce));
$html .= '<div class="row g-3">';
// Full name field
$html .= '<div class="col-md-6">';
$html .= sprintf(
'<input type="text" class="form-control" id="roiContactFullName" name="fullName" placeholder="%s" required>',
esc_attr($fullnamePlaceholder)
);
$html .= '</div>';
// Company field
$html .= '<div class="col-md-6">';
$html .= sprintf(
'<input type="text" class="form-control" id="roiContactCompany" name="company" placeholder="%s">',
esc_attr($companyPlaceholder)
);
$html .= '</div>';
// WhatsApp field
$html .= '<div class="col-md-6">';
$html .= sprintf(
'<input type="tel" class="form-control" id="roiContactWhatsapp" name="whatsapp" placeholder="%s" required>',
esc_attr($whatsappPlaceholder)
);
$html .= '</div>';
// Email field
$html .= '<div class="col-md-6">';
$html .= sprintf(
'<input type="email" class="form-control" id="roiContactEmail" name="email" placeholder="%s" required>',
esc_attr($emailPlaceholder)
);
$html .= '</div>';
// Message field
$html .= '<div class="col-12">';
$html .= sprintf(
'<textarea class="form-control" id="roiContactMessage" name="message" rows="%s" placeholder="%s"></textarea>',
esc_attr($textareaRows),
esc_attr($messagePlaceholder)
);
$html .= '</div>';
// Submit button
$html .= '<div class="col-12">';
$html .= '<button type="submit" class="btn btn-contact-submit w-100">';
$html .= sprintf('<i class="%s me-2"></i>', esc_attr($submitIcon));
$html .= esc_html($submitText);
$html .= '</button>';
$html .= '</div>';
// Message container
$html .= '<div id="roiContactFormMessage" class="col-12 mt-2 alert" style="display: none;"></div>';
$html .= '</div>'; // .row g-3
$html .= '</form>';
$html .= '</div>'; // .col-lg-7
$html .= '</div>'; // .row
$html .= '</div>'; // .col-lg-10
$html .= '</div>'; // .row justify-content-center
$html .= '</div>'; // .container
$html .= '</section>';
return $html;
}
private function buildContactInfoHTML(array $contactInfo): string
{
$phoneLabel = $contactInfo['phone_label'] ?? 'Teléfono';
$phoneValue = $contactInfo['phone_value'] ?? '+52 55 1234 5678';
$emailLabel = $contactInfo['email_label'] ?? 'Email';
$emailValue = $contactInfo['email_value'] ?? 'contacto@apumexico.com';
$locationLabel = $contactInfo['location_label'] ?? 'Ubicación';
$locationValue = $contactInfo['location_value'] ?? 'Ciudad de México, México';
$html = '<div class="contact-info">';
// Phone
$html .= '<div class="d-flex align-items-start mb-3">';
$html .= '<i class="bi bi-telephone-fill me-3 fs-5 contact-icon"></i>';
$html .= '<div>';
$html .= sprintf('<h6 class="mb-1 info-label">%s</h6>', esc_html($phoneLabel));
$html .= sprintf('<p class="mb-0 info-value">%s</p>', esc_html($phoneValue));
$html .= '</div>';
$html .= '</div>';
// Email
$html .= '<div class="d-flex align-items-start mb-3">';
$html .= '<i class="bi bi-envelope-fill me-3 fs-5 contact-icon"></i>';
$html .= '<div>';
$html .= sprintf('<h6 class="mb-1 info-label">%s</h6>', esc_html($emailLabel));
$html .= sprintf('<p class="mb-0 info-value">%s</p>', esc_html($emailValue));
$html .= '</div>';
$html .= '</div>';
// Location
$html .= '<div class="d-flex align-items-start">';
$html .= '<i class="bi bi-geo-alt-fill me-3 fs-5 contact-icon"></i>';
$html .= '<div>';
$html .= sprintf('<h6 class="mb-1 info-label">%s</h6>', esc_html($locationLabel));
$html .= sprintf('<p class="mb-0 info-value">%s</p>', esc_html($locationValue));
$html .= '</div>';
$html .= '</div>';
$html .= '</div>';
return $html;
}
private function buildJS(array $data): string
{
$messages = $data['messages'] ?? [];
$content = $data['content'] ?? [];
$successMessage = $messages['success_message'] ?? '¡Gracias por contactarnos! Te responderemos pronto.';
$errorMessage = $messages['error_message'] ?? 'Hubo un error al enviar el mensaje. Por favor intenta de nuevo.';
$sendingMessage = $messages['sending_message'] ?? 'Enviando...';
$submitText = $content['submit_button_text'] ?? 'Enviar Mensaje';
$submitIcon = $content['submit_button_icon'] ?? 'bi-send-fill';
// AJAX URL for WordPress
$ajaxUrl = admin_url('admin-ajax.php');
$js = <<<JS
(function() {
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('roiContactForm');
if (!form) return;
form.addEventListener('submit', async function(e) {
e.preventDefault();
const submitBtn = form.querySelector('button[type="submit"]');
const messageDiv = document.getElementById('roiContactFormMessage');
const originalBtnHtml = submitBtn.innerHTML;
const nonce = form.dataset.nonce;
// Disable button and show sending state
submitBtn.disabled = true;
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>' + '{$sendingMessage}';
messageDiv.style.display = 'none';
// Collect form data
const formData = new FormData(form);
formData.append('action', 'roi_contact_form_submit');
formData.append('nonce', nonce);
formData.append('pageUrl', window.location.href);
formData.append('pageTitle', document.title);
try {
const response = await fetch('{$ajaxUrl}', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
messageDiv.className = 'col-12 mt-2 alert alert-success';
messageDiv.textContent = '{$successMessage}';
messageDiv.style.display = 'block';
form.reset();
} else {
messageDiv.className = 'col-12 mt-2 alert alert-danger';
messageDiv.textContent = result.data?.message || '{$errorMessage}';
messageDiv.style.display = 'block';
}
} catch (error) {
console.error('Contact form error:', error);
messageDiv.className = 'col-12 mt-2 alert alert-danger';
messageDiv.textContent = '{$errorMessage}';
messageDiv.style.display = 'block';
} finally {
submitBtn.disabled = false;
submitBtn.innerHTML = originalBtnHtml;
}
});
});
})();
JS;
return $js;
}
}

View File

@@ -0,0 +1,280 @@
<?php
declare(strict_types=1);
namespace ROITheme\Public\CtaBoxSidebar\Infrastructure\Ui;
use ROITheme\Shared\Domain\Contracts\RendererInterface;
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
use ROITheme\Shared\Domain\Entities\Component;
/**
* CtaBoxSidebarRenderer - Renderiza caja CTA en sidebar
*
* RESPONSABILIDAD: Generar HTML y CSS del CTA Box Sidebar
*
* CARACTERISTICAS:
* - Titulo configurable
* - Descripcion configurable
* - Boton con icono y multiples acciones (modal, link, scroll)
* - Estilos 100% desde BD via CSSGenerator
*
* Cumple con:
* - DIP: Recibe CSSGeneratorInterface por constructor
* - SRP: Una responsabilidad (renderizar CTA box)
* - Clean Architecture: Infrastructure puede usar WordPress
*
* @package ROITheme\Public\CtaBoxSidebar\Infrastructure\Ui
*/
final class CtaBoxSidebarRenderer implements RendererInterface
{
public function __construct(
private CSSGeneratorInterface $cssGenerator
) {}
public function render(Component $component): string
{
$data = $component->getData();
if (!$this->isEnabled($data)) {
return '';
}
if (!$this->shouldShowOnCurrentPage($data)) {
return '';
}
$css = $this->generateCSS($data);
$html = $this->buildHTML($data);
$script = $this->buildScript();
return sprintf("<style>%s</style>\n%s\n%s", $css, $html, $script);
}
public function supports(string $componentType): bool
{
return $componentType === 'cta-box-sidebar';
}
private function isEnabled(array $data): bool
{
return ($data['visibility']['is_enabled'] ?? false) === true;
}
private function shouldShowOnCurrentPage(array $data): bool
{
$showOn = $data['visibility']['show_on_pages'] ?? 'posts';
switch ($showOn) {
case 'all':
return true;
case 'posts':
return is_single();
case 'pages':
return is_page();
default:
return true;
}
}
private function generateCSS(array $data): string
{
$colors = $data['colors'] ?? [];
$spacing = $data['spacing'] ?? [];
$typography = $data['typography'] ?? [];
$effects = $data['visual_effects'] ?? [];
$behavior = $data['behavior'] ?? [];
$visibility = $data['visibility'] ?? [];
$cssRules = [];
$transitionDuration = $effects['transition_duration'] ?? '0.3s';
// Container styles - Match template exactly (height: 250px, flexbox centering)
$cssRules[] = $this->cssGenerator->generate('.cta-box-sidebar', [
'background' => $colors['background_color'] ?? '#FF8600',
'border-radius' => $effects['border_radius'] ?? '8px',
'padding' => $spacing['container_padding'] ?? '24px',
'text-align' => $behavior['text_align'] ?? 'center',
'box-shadow' => $effects['box_shadow'] ?? '0 4px 12px rgba(255, 133, 0, 0.2)',
'margin-top' => '0',
'margin-bottom' => '15px',
'height' => '250px',
'display' => 'flex',
'flex-direction' => 'column',
'justify-content' => 'center',
]);
// Title styles
$cssRules[] = $this->cssGenerator->generate('.cta-box-sidebar .cta-box-title', [
'color' => $colors['title_color'] ?? '#ffffff',
'font-weight' => $typography['title_font_weight'] ?? '700',
'font-size' => $typography['title_font_size'] ?? '1.25rem',
'margin-bottom' => $spacing['title_margin_bottom'] ?? '1rem',
'margin-top' => '0',
]);
// Description styles
$cssRules[] = $this->cssGenerator->generate('.cta-box-sidebar .cta-box-text', [
'color' => $colors['description_color'] ?? 'rgba(255, 255, 255, 0.95)',
'font-size' => $typography['description_font_size'] ?? '0.9rem',
'margin-bottom' => $spacing['description_margin_bottom'] ?? '1rem',
]);
// Button styles
$cssRules[] = $this->cssGenerator->generate('.cta-box-sidebar .btn-cta-box', [
'background-color' => $colors['button_background_color'] ?? '#ffffff',
'color' => $colors['button_text_color'] ?? '#FF8600',
'font-weight' => $typography['button_font_weight'] ?? '700',
'font-size' => $typography['button_font_size'] ?? '1rem',
'border' => 'none',
'padding' => $spacing['button_padding'] ?? '0.75rem 1.5rem',
'border-radius' => $effects['button_border_radius'] ?? '8px',
'transition' => "all {$transitionDuration} ease",
'cursor' => 'pointer',
'display' => 'inline-flex',
'align-items' => 'center',
'justify-content' => 'center',
'width' => '100%',
]);
// Button hover styles (template uses --color-navy-primary = #1e3a5f)
$cssRules[] = $this->cssGenerator->generate('.cta-box-sidebar .btn-cta-box:hover', [
'background-color' => $colors['button_hover_background'] ?? '#1e3a5f',
'color' => $colors['button_hover_text_color'] ?? '#ffffff',
]);
// Button icon spacing
$cssRules[] = $this->cssGenerator->generate('.cta-box-sidebar .btn-cta-box i', [
'margin-right' => $spacing['icon_margin_right'] ?? '0.5rem',
]);
// Responsive visibility
$showOnDesktop = $visibility['show_on_desktop'] ?? true;
$showOnMobile = $visibility['show_on_mobile'] ?? false;
if (!$showOnMobile) {
$cssRules[] = "@media (max-width: 991.98px) {
.cta-box-sidebar { display: none !important; }
}";
}
if (!$showOnDesktop) {
$cssRules[] = "@media (min-width: 992px) {
.cta-box-sidebar { display: none !important; }
}";
}
return implode("\n", $cssRules);
}
private function buildHTML(array $data): string
{
$content = $data['content'] ?? [];
$title = $content['title'] ?? '¿Listo para potenciar tus proyectos?';
$description = $content['description'] ?? 'Accede a nuestra biblioteca completa de APUs y herramientas profesionales.';
$buttonText = $content['button_text'] ?? 'Solicitar Demo';
$buttonIcon = $content['button_icon'] ?? 'bi bi-calendar-check';
$buttonAction = $content['button_action'] ?? 'modal';
$buttonLink = $content['button_link'] ?? '#contactModal';
// Build button attributes based on action type
$buttonAttributes = $this->getButtonAttributes($buttonAction, $buttonLink);
$html = '<div class="cta-box-sidebar">';
// Title
$html .= sprintf(
'<h5 class="cta-box-title">%s</h5>',
esc_html($title)
);
// Description
$html .= sprintf(
'<p class="cta-box-text">%s</p>',
esc_html($description)
);
// Button
$iconHtml = !empty($buttonIcon)
? sprintf('<i class="%s"></i>', esc_attr($buttonIcon))
: '';
$html .= sprintf(
'<button class="btn btn-cta-box" %s>%s%s</button>',
$buttonAttributes,
$iconHtml,
esc_html($buttonText)
);
$html .= '</div>';
return $html;
}
private function getButtonAttributes(string $action, string $link): string
{
switch ($action) {
case 'modal':
// Extract modal ID from link (e.g., #contactModal -> contactModal)
$modalId = ltrim($link, '#');
return sprintf(
'type="button" data-bs-toggle="modal" data-bs-target="#%s"',
esc_attr($modalId)
);
case 'link':
return sprintf(
'type="button" data-cta-action="link" data-cta-href="%s"',
esc_url($link)
);
case 'scroll':
$targetId = ltrim($link, '#');
return sprintf(
'type="button" data-cta-action="scroll" data-cta-target="%s"',
esc_attr($targetId)
);
default:
return 'type="button"';
}
}
private function getVisibilityClasses(bool $desktop, bool $mobile): ?string
{
if (!$desktop && !$mobile) {
return null;
}
if (!$desktop && $mobile) {
return 'd-lg-none';
}
if ($desktop && !$mobile) {
return 'd-none d-lg-block';
}
return '';
}
private function buildScript(): string
{
return <<<JS
<script>
document.addEventListener('DOMContentLoaded', function() {
var ctaButtons = document.querySelectorAll('.btn-cta-box[data-cta-action]');
ctaButtons.forEach(function(btn) {
btn.addEventListener('click', function() {
var action = this.getAttribute('data-cta-action');
if (action === 'link') {
var href = this.getAttribute('data-cta-href');
if (href) window.location.href = href;
} else if (action === 'scroll') {
var target = this.getAttribute('data-cta-target');
var el = document.getElementById(target);
if (el) el.scrollIntoView({behavior: 'smooth'});
}
});
});
});
</script>
JS;
}
}

View File

@@ -0,0 +1,360 @@
<?php
declare(strict_types=1);
namespace ROITheme\Public\CtaLetsTalk\Infrastructure\Ui;
use ROITheme\Shared\Domain\Contracts\RendererInterface;
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
use ROITheme\Shared\Domain\Entities\Component;
/**
* Class CtaLetsTalkRenderer
*
* Renderizador del componente CTA "Let's Talk" para el frontend.
*
* Responsabilidades:
* - Renderizar botón CTA "Let's Talk" en el navbar
* - Delegar generación de CSS a CSSGeneratorInterface
* - Validar visibilidad (is_enabled, show_on_pages, show_on_desktop, show_on_mobile)
* - Manejar visibilidad responsive con clases Bootstrap
* - Generar atributos para modal o URL personalizada
* - Sanitizar todos los outputs
*
* NO responsable de:
* - Generar string CSS (delega a CSSGeneratorService)
* - Persistir datos (ya están en Component)
* - Lógica de negocio (está en Domain)
*
* Cumple con:
* - DIP: Recibe CSSGeneratorInterface por constructor
* - SRP: Una responsabilidad (renderizar este componente)
* - Clean Architecture: Infrastructure puede usar WordPress
*
* @package ROITheme\Public\CtaLetsTalk\Infrastructure\Ui
*/
final class CtaLetsTalkRenderer implements RendererInterface
{
/**
* @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS
*/
public function __construct(
private CSSGeneratorInterface $cssGenerator
) {}
/**
* {@inheritDoc}
*/
public function render(Component $component): string
{
$data = $component->getData();
// Validar visibilidad general
if (!$this->isEnabled($data)) {
return '';
}
// Validar visibilidad por página
if (!$this->shouldShowOnCurrentPage($data)) {
return '';
}
// Generar CSS usando CSSGeneratorService
$css = $this->generateCSS($data);
// Generar HTML
$html = $this->buildHTML($data);
// Combinar todo
return sprintf(
"<style>%s</style>\n%s",
$css,
$html
);
}
/**
* {@inheritDoc}
*/
public function supports(string $componentType): bool
{
return $componentType === 'cta-lets-talk';
}
/**
* Verificar si el componente está habilitado
*
* @param array $data Datos del componente
* @return bool
*/
private function isEnabled(array $data): bool
{
return ($data['visibility']['is_enabled'] ?? false) === true;
}
/**
* Verificar si debe mostrarse en la página actual
*
* @param array $data Datos del componente
* @return bool
*/
private function shouldShowOnCurrentPage(array $data): bool
{
$showOn = $data['visibility']['show_on_pages'] ?? 'all';
return match ($showOn) {
'all' => true,
'home' => is_front_page(),
'posts' => is_single(),
'pages' => is_page(),
default => true,
};
}
/**
* Calcular clases de visibilidad responsive
*
* @param bool $desktop Mostrar en desktop
* @param bool $mobile Mostrar en mobile
* @return string|null Clases CSS o null si no debe mostrarse
*/
private function getVisibilityClasses(bool $desktop, bool $mobile): ?string
{
if (!$desktop && !$mobile) {
return null;
}
if (!$desktop && $mobile) {
return 'd-lg-none';
}
if ($desktop && !$mobile) {
return 'd-none d-lg-block';
}
return '';
}
/**
* Generar CSS usando CSSGeneratorService
*
* @param array $data Datos del componente
* @return string CSS generado
*/
private function generateCSS(array $data): string
{
$css = '';
// Estilos base del botón
$baseStyles = [
'background_color' => $data['colors']['background_color'] ?? '#FF8600',
'color' => $data['colors']['text_color'] ?? '#FFFFFF',
'font_size' => $data['typography']['font_size'] ?? '1rem',
'font_weight' => $data['typography']['font_weight'] ?? '600',
'text_transform' => $data['typography']['text_transform'] ?? 'none',
'padding' => sprintf(
'%s %s',
$data['spacing']['padding_top_bottom'] ?? '0.5rem',
$data['spacing']['padding_left_right'] ?? '1.5rem'
),
'border' => sprintf(
'%s solid %s',
$data['visual_effects']['border_width'] ?? '0',
$data['colors']['border_color'] ?? 'transparent'
),
'border_radius' => $data['visual_effects']['border_radius'] ?? '6px',
'box_shadow' => $data['visual_effects']['box_shadow'] ?? 'none',
'transition' => sprintf(
'all %s ease',
$data['visual_effects']['transition_duration'] ?? '0.3s'
),
'cursor' => 'pointer',
];
$css .= $this->cssGenerator->generate('.btn-lets-talk', $baseStyles);
// Estilos hover del botón
$hoverStyles = [
'background_color' => $data['colors']['background_hover_color'] ?? '#FF6B35',
'color' => $data['colors']['text_hover_color'] ?? '#FFFFFF',
];
$css .= "\n" . $this->cssGenerator->generate('.btn-lets-talk:hover', $hoverStyles);
// Estilos del ícono dentro del botón
$iconStyles = [
'color' => $data['colors']['text_color'] ?? '#FFFFFF',
'margin_right' => $data['spacing']['icon_spacing'] ?? '0.5rem',
];
$css .= "\n" . $this->cssGenerator->generate('.btn-lets-talk i', $iconStyles);
// Estilos responsive - ocultar en móvil si show_on_mobile = false
$showOnMobile = ($data['visibility']['show_on_mobile'] ?? false) === true;
if (!$showOnMobile) {
$responsiveStyles = [
'display' => 'none !important',
];
$css .= "\n@media (max-width: 991px) {\n";
$css .= $this->cssGenerator->generate('.btn-lets-talk', $responsiveStyles);
$css .= "\n}";
}
// Estilos responsive - ocultar en desktop si show_on_desktop = false
$showOnDesktop = ($data['visibility']['show_on_desktop'] ?? true) === true;
if (!$showOnDesktop) {
$responsiveStyles = [
'display' => 'none !important',
];
$css .= "\n@media (min-width: 992px) {\n";
$css .= $this->cssGenerator->generate('.btn-lets-talk', $responsiveStyles);
$css .= "\n}";
}
// Margen izquierdo para separar del menú (solo desktop)
$marginLeft = $data['spacing']['margin_left'] ?? '1rem';
if (!empty($marginLeft) && $marginLeft !== '0') {
$css .= "\n@media (min-width: 992px) {\n";
$css .= $this->cssGenerator->generate('.btn-lets-talk', ['margin_left' => $marginLeft]);
$css .= "\n}";
}
return $css;
}
/**
* Generar HTML del componente
*
* @param array $data Datos del componente
* @return string HTML generado
*/
private function buildHTML(array $data): string
{
$classes = $this->buildClasses($data);
$attributes = $this->buildAttributes($data);
$content = $this->buildContent($data);
$tag = $this->useModal($data) ? 'button' : 'a';
return sprintf(
'<%s class="%s"%s>%s</%s>',
$tag,
esc_attr($classes),
$attributes,
$content,
$tag
);
}
/**
* Construir clases CSS del componente
*
* @param array $data Datos del componente
* @return string Clases CSS
*/
private function buildClasses(array $data): string
{
$classes = ['btn', 'btn-lets-talk'];
// Agregar clase ms-lg-3 para margen en desktop (Bootstrap)
// Esto solo aplica en pantallas >= lg (992px)
$classes[] = 'ms-lg-3';
return implode(' ', $classes);
}
/**
* Determinar si debe usar modal o URL
*
* @param array $data Datos del componente
* @return bool
*/
private function useModal(array $data): bool
{
return ($data['behavior']['enable_modal'] ?? true) === true;
}
/**
* Construir atributos HTML del componente
*
* @param array $data Datos del componente
* @return string Atributos HTML
*/
private function buildAttributes(array $data): string
{
$attributes = [];
if ($this->useModal($data)) {
// Atributos para modal de Bootstrap
$attributes[] = 'type="button"';
$attributes[] = 'data-bs-toggle="modal"';
$modalTarget = $data['content']['modal_target'] ?? '#contactModal';
$attributes[] = sprintf('data-bs-target="%s"', esc_attr($modalTarget));
} else {
// Atributos para enlace
$customUrl = $data['behavior']['custom_url'] ?? '';
$attributes[] = sprintf('href="%s"', esc_url($customUrl ?: '#'));
if (($data['behavior']['open_in_new_tab'] ?? false) === true) {
$attributes[] = 'target="_blank"';
$attributes[] = 'rel="noopener noreferrer"';
}
}
// Atributo ARIA para accesibilidad
$ariaLabel = $data['content']['aria_label'] ?? 'Abrir formulario de contacto';
if (!empty($ariaLabel)) {
$attributes[] = sprintf('aria-label="%s"', esc_attr($ariaLabel));
}
return !empty($attributes) ? ' ' . implode(' ', $attributes) : '';
}
/**
* Construir contenido del botón
*
* @param array $data Datos del componente
* @return string HTML del contenido
*/
private function buildContent(array $data): string
{
$html = '';
// Ícono (si está habilitado)
if ($this->shouldShowIcon($data)) {
$html .= $this->buildIcon($data);
}
// Texto del botón
$buttonText = $data['content']['button_text'] ?? "Let's Talk";
$html .= esc_html($buttonText);
return $html;
}
/**
* Verificar si debe mostrar el ícono
*
* @param array $data Datos del componente
* @return bool
*/
private function shouldShowIcon(array $data): bool
{
return ($data['content']['show_icon'] ?? true) === true;
}
/**
* Construir ícono del componente
*
* @param array $data Datos del componente
* @return string HTML del ícono
*/
private function buildIcon(array $data): string
{
$iconClass = $data['content']['icon_class'] ?? 'bi-lightning-charge-fill';
// Asegurar prefijo 'bi-'
if (strpos($iconClass, 'bi-') !== 0) {
$iconClass = 'bi-' . $iconClass;
}
return sprintf(
'<i class="bi %s"></i>',
esc_attr($iconClass)
);
}
}

View File

@@ -0,0 +1,187 @@
<?php
declare(strict_types=1);
namespace ROITheme\Public\CtaPost\Infrastructure\Ui;
use ROITheme\Shared\Domain\Contracts\RendererInterface;
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
use ROITheme\Shared\Domain\Entities\Component;
/**
* CtaPostRenderer - Renderiza CTA promocional debajo del contenido
*
* RESPONSABILIDAD: Generar HTML y CSS del componente CTA Post
*
* CARACTERISTICAS:
* - Gradiente configurable
* - Layout responsive (2 columnas en desktop)
* - Boton CTA con icono
* - Estilos 100% desde BD via CSSGenerator
*
* @package ROITheme\Public\CtaPost\Infrastructure\Ui
*/
final class CtaPostRenderer implements RendererInterface
{
public function __construct(
private CSSGeneratorInterface $cssGenerator
) {}
public function render(Component $component): string
{
$data = $component->getData();
if (!$this->isEnabled($data)) {
return '';
}
if (!$this->shouldShowOnCurrentPage($data)) {
return '';
}
$css = $this->generateCSS($data);
$html = $this->buildHTML($data);
return sprintf("<style>%s</style>\n%s", $css, $html);
}
public function supports(string $componentType): bool
{
return $componentType === 'cta-post';
}
private function isEnabled(array $data): bool
{
$value = $data['visibility']['is_enabled'] ?? false;
return $value === true || $value === '1' || $value === 1;
}
private function shouldShowOnCurrentPage(array $data): bool
{
$showOn = $data['visibility']['show_on_pages'] ?? 'posts';
switch ($showOn) {
case 'all':
return true;
case 'posts':
return is_single();
case 'pages':
return is_page();
default:
return true;
}
}
private function generateCSS(array $data): string
{
$colors = $data['colors'] ?? [];
$effects = $data['visual_effects'] ?? [];
$visibility = $data['visibility'] ?? [];
$cssRules = [];
$gradientStart = $colors['gradient_start'] ?? '#FF8600';
$gradientEnd = $colors['gradient_end'] ?? '#FFB800';
$gradientAngle = $effects['gradient_angle'] ?? '135deg';
$buttonBgColor = $colors['button_bg_color'] ?? '#FF8600';
$buttonTextColor = $colors['button_text_color'] ?? '#ffffff';
$buttonHoverBgColor = $colors['button_hover_bg_color'] ?? '#e67a00';
// Container - gradient background
$cssRules[] = $this->cssGenerator->generate('.cta-post-container', [
'background' => "linear-gradient({$gradientAngle}, {$gradientStart} 0%, {$gradientEnd} 100%)",
]);
// Button styles (matching template .cta-button) - Using !important to override Bootstrap btn-light
$cssRules[] = ".cta-post-container .cta-button {
background-color: {$buttonBgColor} !important;
color: {$buttonTextColor} !important;
font-weight: 600;
padding: 0.75rem 2rem;
border: none !important;
border-radius: 8px;
transition: 0.3s;
text-decoration: none;
display: inline-block;
}";
// Button hover state
$cssRules[] = ".cta-post-container .cta-button:hover {
background-color: {$buttonHoverBgColor};
color: {$buttonTextColor};
}";
// Responsive: button full width on mobile
$cssRules[] = "@media (max-width: 768px) {
.cta-post-container .cta-button {
width: 100%;
margin-top: 1rem;
}
}";
// Responsive visibility
$showOnDesktop = $visibility['show_on_desktop'] ?? true;
$showOnDesktop = $showOnDesktop === true || $showOnDesktop === '1' || $showOnDesktop === 1;
$showOnMobile = $visibility['show_on_mobile'] ?? true;
$showOnMobile = $showOnMobile === true || $showOnMobile === '1' || $showOnMobile === 1;
if (!$showOnMobile) {
$cssRules[] = "@media (max-width: 991.98px) {
.cta-post-container { display: none !important; }
}";
}
if (!$showOnDesktop) {
$cssRules[] = "@media (min-width: 992px) {
.cta-post-container { display: none !important; }
}";
}
return implode("\n", $cssRules);
}
private function buildHTML(array $data): string
{
$content = $data['content'] ?? [];
$title = $content['title'] ?? 'Accede a 200,000+ Análisis de Precios Unitarios';
$description = $content['description'] ?? '';
$buttonText = $content['button_text'] ?? 'Ver Catálogo Completo';
$buttonUrl = $content['button_url'] ?? '#';
$buttonIcon = $content['button_icon'] ?? 'bi-arrow-right';
$html = '<div class="my-5 p-4 rounded cta-post-container">';
$html .= ' <div class="row align-items-center">';
// Left column - Content
$html .= ' <div class="col-md-8">';
$html .= sprintf(
' <h3 class="h4 fw-bold text-white mb-2">%s</h3>',
esc_html($title)
);
if (!empty($description)) {
$html .= sprintf(
' <p class="text-white mb-md-0">%s</p>',
esc_html($description)
);
}
$html .= ' </div>';
// Right column - Button
$html .= ' <div class="col-md-4 text-md-end mt-3 mt-md-0">';
$html .= sprintf(
' <a href="%s" class="btn btn-light btn-lg cta-button">%s',
esc_url($buttonUrl),
esc_html($buttonText)
);
if (!empty($buttonIcon)) {
$html .= sprintf(' <i class="bi %s ms-2"></i>', esc_attr($buttonIcon));
}
$html .= '</a>';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
}

View File

@@ -0,0 +1,202 @@
<?php
declare(strict_types=1);
namespace ROITheme\Public\FeaturedImage\Infrastructure\Ui;
use ROITheme\Shared\Domain\Contracts\RendererInterface;
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
use ROITheme\Shared\Domain\Entities\Component;
/**
* FeaturedImageRenderer - Renderiza la imagen destacada del post
*
* RESPONSABILIDAD: Generar HTML y CSS de la imagen destacada
*
* CARACTERISTICAS:
* - Integracion con get_the_post_thumbnail()
* - Estilos configurables desde BD
* - Efecto hover opcional
* - Soporte responsive
*
* Cumple con:
* - DIP: Recibe CSSGeneratorInterface por constructor
* - SRP: Una responsabilidad (renderizar featured image)
* - Clean Architecture: Infrastructure puede usar WordPress
*
* @package ROITheme\Public\FeaturedImage\Infrastructure\Ui
*/
final class FeaturedImageRenderer implements RendererInterface
{
public function __construct(
private CSSGeneratorInterface $cssGenerator
) {}
public function render(Component $component): string
{
$data = $component->getData();
if (!$this->isEnabled($data)) {
return '';
}
if (!$this->shouldShowOnCurrentPage($data)) {
return '';
}
if (!$this->hasPostThumbnail()) {
return '';
}
$css = $this->generateCSS($data);
$html = $this->buildHTML($data);
return sprintf("<style>%s</style>\n%s", $css, $html);
}
public function supports(string $componentType): bool
{
return $componentType === 'featured-image';
}
private function isEnabled(array $data): bool
{
return ($data['visibility']['is_enabled'] ?? false) === true;
}
private function shouldShowOnCurrentPage(array $data): bool
{
$showOn = $data['visibility']['show_on_pages'] ?? 'posts';
switch ($showOn) {
case 'all':
return true;
case 'posts':
return is_single();
case 'pages':
return is_page();
default:
return true;
}
}
private function hasPostThumbnail(): bool
{
return is_singular() && has_post_thumbnail();
}
private function generateCSS(array $data): string
{
$spacing = $data['spacing'] ?? [];
$effects = $data['visual_effects'] ?? [];
$visibility = $data['visibility'] ?? [];
$marginTop = $spacing['margin_top'] ?? '1rem';
$marginBottom = $spacing['margin_bottom'] ?? '2rem';
$borderRadius = $effects['border_radius'] ?? '12px';
$boxShadow = $effects['box_shadow'] ?? '0 8px 24px rgba(0, 0, 0, 0.1)';
$hoverEffect = $effects['hover_effect'] ?? true;
$hoverScale = $effects['hover_scale'] ?? '1.02';
$transitionDuration = $effects['transition_duration'] ?? '0.3s';
$showOnDesktop = $visibility['show_on_desktop'] ?? true;
$showOnMobile = $visibility['show_on_mobile'] ?? true;
$cssRules = [];
// Container styles
$cssRules[] = $this->cssGenerator->generate('.featured-image-container', [
'border-radius' => $borderRadius,
'overflow' => 'hidden',
'box-shadow' => $boxShadow,
'margin-top' => $marginTop,
'margin-bottom' => $marginBottom,
'transition' => "transform {$transitionDuration} ease, box-shadow {$transitionDuration} ease",
]);
// Image styles
$cssRules[] = $this->cssGenerator->generate('.featured-image-container img', [
'width' => '100%',
'height' => 'auto',
'display' => 'block',
'transition' => "transform {$transitionDuration} ease",
]);
// Hover effect
if ($hoverEffect) {
$cssRules[] = $this->cssGenerator->generate('.featured-image-container:hover', [
'box-shadow' => '0 12px 32px rgba(0, 0, 0, 0.15)',
]);
$cssRules[] = $this->cssGenerator->generate('.featured-image-container:hover img', [
'transform' => "scale({$hoverScale})",
]);
}
// Link styles (remove default link styling)
$cssRules[] = $this->cssGenerator->generate('.featured-image-container a', [
'display' => 'block',
'line-height' => '0',
]);
// Responsive visibility
if (!$showOnMobile) {
$cssRules[] = "@media (max-width: 767.98px) {
.featured-image-container { display: none !important; }
}";
}
if (!$showOnDesktop) {
$cssRules[] = "@media (min-width: 768px) {
.featured-image-container { display: none !important; }
}";
}
return implode("\n", $cssRules);
}
private function buildHTML(array $data): string
{
$content = $data['content'] ?? [];
$imageSize = $content['image_size'] ?? 'roi-featured-large';
$lazyLoading = $content['lazy_loading'] ?? true;
$linkToMedia = $content['link_to_media'] ?? false;
$imgAttr = [
'class' => 'img-fluid featured-image',
'alt' => get_the_title(),
];
if ($lazyLoading) {
$imgAttr['loading'] = 'lazy';
}
$thumbnail = get_the_post_thumbnail(null, $imageSize, $imgAttr);
if (empty($thumbnail)) {
return '';
}
$html = '<div class="featured-image-container">';
if ($linkToMedia) {
$fullImageUrl = get_the_post_thumbnail_url(null, 'full');
$html .= sprintf(
'<a href="%s" target="_blank" rel="noopener" aria-label="%s">',
esc_url($fullImageUrl),
esc_attr__('Ver imagen en tamano completo', 'roi-theme')
);
}
$html .= $thumbnail;
if ($linkToMedia) {
$html .= '</a>';
}
$html .= '</div>';
return $html;
}
}

View File

@@ -0,0 +1,185 @@
<?php
declare(strict_types=1);
namespace ROITheme\Public\Footer\Infrastructure\Api\WordPress;
use ROITheme\Shared\Domain\Contracts\ComponentSettingsRepositoryInterface;
/**
* NewsletterAjaxHandler - Procesa suscripciones al newsletter
*
* RESPONSABILIDAD: Recibir email y enviarlo al webhook configurado
*
* SEGURIDAD:
* - Verifica nonce
* - Webhook URL nunca se expone al cliente
* - Rate limiting basico
* - Sanitizacion de inputs
*
* @package ROITheme\Public\Footer\Infrastructure\Api\WordPress
*/
final class NewsletterAjaxHandler
{
private const NONCE_ACTION = 'roi_newsletter_nonce';
private const COMPONENT_NAME = 'footer';
public function __construct(
private ComponentSettingsRepositoryInterface $settingsRepository
) {}
/**
* Registrar hooks AJAX
*/
public function register(): void
{
add_action('wp_ajax_roi_newsletter_subscribe', [$this, 'handleSubscribe']);
add_action('wp_ajax_nopriv_roi_newsletter_subscribe', [$this, 'handleSubscribe']);
}
/**
* Procesar suscripcion
*/
public function handleSubscribe(): void
{
// 1. Verificar nonce
$nonce = sanitize_text_field($_POST['nonce'] ?? '');
if (!wp_verify_nonce($nonce, self::NONCE_ACTION)) {
wp_send_json_error([
'message' => __('Error de seguridad. Por favor recarga la pagina.', 'roi-theme')
], 403);
return;
}
// 2. Rate limiting (1 suscripcion por IP cada 60 segundos)
if (!$this->checkRateLimit()) {
wp_send_json_error([
'message' => __('Por favor espera un momento antes de intentar de nuevo.', 'roi-theme')
], 429);
return;
}
// 3. Validar email
$email = sanitize_email($_POST['email'] ?? '');
if (empty($email) || !is_email($email)) {
wp_send_json_error([
'message' => __('Por favor ingresa un email valido.', 'roi-theme')
], 422);
return;
}
// 4. Obtener configuracion del componente
$settings = $this->settingsRepository->getComponentSettings(self::COMPONENT_NAME);
if (empty($settings)) {
wp_send_json_error([
'message' => __('Error de configuracion. Contacta al administrador.', 'roi-theme')
], 500);
return;
}
$newsletter = $settings['newsletter'] ?? [];
$webhookUrl = $newsletter['newsletter_webhook_url'] ?? '';
$successMsg = $newsletter['newsletter_success_message'] ?? __('Gracias por suscribirte!', 'roi-theme');
$errorMsg = $newsletter['newsletter_error_message'] ?? __('Error al suscribirse. Intenta de nuevo.', 'roi-theme');
if (empty($webhookUrl)) {
// Si no hay webhook, simular exito para UX pero loguear warning
error_log('ROI Theme Newsletter: No webhook URL configured');
wp_send_json_success([
'message' => $successMsg
]);
return;
}
// 5. Preparar payload
$payload = [
'email' => $email,
'source' => 'newsletter-footer',
'timestamp' => current_time('c'),
'siteName' => get_bloginfo('name'),
'siteUrl' => home_url(),
];
// 6. Enviar a webhook
$result = $this->sendToWebhook($webhookUrl, $payload);
if ($result['success']) {
wp_send_json_success([
'message' => $successMsg
]);
} else {
error_log('ROI Theme Newsletter webhook error: ' . $result['error']);
wp_send_json_error([
'message' => $errorMsg
], 500);
}
}
/**
* Enviar datos al webhook
*/
private function sendToWebhook(string $url, array $payload): array
{
$response = wp_remote_post($url, [
'timeout' => 30,
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'body' => wp_json_encode($payload),
]);
if (is_wp_error($response)) {
return [
'success' => false,
'error' => $response->get_error_message()
];
}
$statusCode = wp_remote_retrieve_response_code($response);
if ($statusCode >= 200 && $statusCode < 300) {
return ['success' => true, 'error' => ''];
}
return [
'success' => false,
'error' => sprintf('HTTP %d: %s', $statusCode, wp_remote_retrieve_response_message($response))
];
}
/**
* Rate limiting por IP
*/
private function checkRateLimit(): bool
{
$ip = $this->getClientIP();
$transientKey = 'roi_newsletter_' . md5($ip);
$lastSubmit = get_transient($transientKey);
if ($lastSubmit !== false) {
return false;
}
set_transient($transientKey, time(), 60);
return true;
}
/**
* Obtener IP del cliente
*/
private function getClientIP(): string
{
$ip = '';
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
$ip = sanitize_text_field($_SERVER['HTTP_CLIENT_IP']);
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip = sanitize_text_field(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0]);
} elseif (!empty($_SERVER['REMOTE_ADDR'])) {
$ip = sanitize_text_field($_SERVER['REMOTE_ADDR']);
}
return $ip;
}
}

View File

@@ -0,0 +1,423 @@
<?php
declare(strict_types=1);
namespace ROITheme\Public\Footer\Infrastructure\Ui;
use ROITheme\Shared\Domain\Contracts\RendererInterface;
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
use ROITheme\Shared\Domain\Entities\Component;
/**
* FooterRenderer - Renderiza el footer del sitio
*
* RESPONSABILIDAD: Generar HTML y CSS del footer con menus WP y newsletter
*
* SEGURIDAD:
* - Webhook URL nunca se expone al cliente
* - Escaping de todos los outputs
* - Nonce para formulario newsletter
*
* @package ROITheme\Public\Footer\Infrastructure\Ui
*/
final class FooterRenderer implements RendererInterface
{
private const NONCE_ACTION = 'roi_newsletter_nonce';
public function __construct(
private CSSGeneratorInterface $cssGenerator
) {}
public function supports(string $componentType): bool
{
return $componentType === 'footer';
}
public function render(Component $component): string
{
$data = $component->getData();
// Validar visibilidad
$visibility = $data['visibility'] ?? [];
if (!($visibility['is_enabled'] ?? true)) {
return '';
}
// Verificar visibilidad responsive
$showDesktop = $visibility['show_on_desktop'] ?? true;
$showMobile = $visibility['show_on_mobile'] ?? true;
if (!$showDesktop && !$showMobile) {
return '';
}
// Generar CSS
$css = $this->generateCSS($data, $showDesktop, $showMobile);
// Generar HTML
$html = $this->generateHTML($data);
// Generar JavaScript
$js = $this->generateJS($data);
return $css . $html . $js;
}
private function generateCSS(array $data, bool $showDesktop, bool $showMobile): string
{
$colors = $data['colors'] ?? [];
$spacing = $data['spacing'] ?? [];
$effects = $data['visual_effects'] ?? [];
// Valores con fallbacks
$bgColor = $colors['bg_color'] ?? '#212529';
$textColor = $colors['text_color'] ?? '#ffffff';
$titleColor = $colors['title_color'] ?? '#ffffff';
$linkColor = $colors['link_color'] ?? '#ffffff';
$linkHoverColor = $colors['link_hover_color'] ?? '#FF8600';
$inputBgColor = $colors['input_bg_color'] ?? '#ffffff';
$inputTextColor = $colors['input_text_color'] ?? '#212529';
$inputBorderColor = $colors['input_border_color'] ?? '#dee2e6';
$buttonBgColor = $colors['button_bg_color'] ?? '#0d6efd';
$buttonTextColor = $colors['button_text_color'] ?? '#ffffff';
$buttonHoverBg = $colors['button_hover_bg'] ?? '#0b5ed7';
$borderTopColor = $colors['border_top_color'] ?? 'rgba(255, 255, 255, 0.2)';
$paddingY = $spacing['padding_y'] ?? '3rem';
$marginTop = $spacing['margin_top'] ?? '0';
$widgetTitleMb = $spacing['widget_title_margin_bottom'] ?? '1rem';
$linkMb = $spacing['link_margin_bottom'] ?? '0.5rem';
$copyrightPy = $spacing['copyright_padding_y'] ?? '1.5rem';
$inputRadius = $effects['input_border_radius'] ?? '6px';
$buttonRadius = $effects['button_border_radius'] ?? '6px';
$transition = $effects['transition_duration'] ?? '0.3s';
$cssRules = [];
// Footer principal
$cssRules[] = $this->cssGenerator->generate('.roi-footer', [
'background-color' => $bgColor,
'color' => $textColor,
'padding-top' => $paddingY,
'padding-bottom' => $paddingY,
'margin-top' => $marginTop,
]);
// Grid custom para 3+3+3+4 = 13 columnas
$cssRules[] = $this->cssGenerator->generate('.roi-footer .footer-grid', [
'display' => 'grid',
'grid-template-columns' => 'repeat(4, 1fr)',
'gap' => '2rem',
]);
// En desktop: distribucion 3+3+3+4
$cssRules[] = "@media (min-width: 768px) {
.roi-footer .footer-grid {
grid-template-columns: 23% 23% 23% 31%;
}
}";
// En mobile: 2 columnas
$cssRules[] = "@media (max-width: 767px) {
.roi-footer .footer-grid {
grid-template-columns: 1fr 1fr;
}
.roi-footer .footer-widget-newsletter {
grid-column: span 2;
}
}";
// Titulos de widgets
$cssRules[] = $this->cssGenerator->generate('.roi-footer .widget-title', [
'color' => $titleColor,
'font-size' => '1.25rem',
'font-weight' => '500',
'margin-bottom' => $widgetTitleMb,
]);
// Links de navegacion
$cssRules[] = $this->cssGenerator->generate('.roi-footer .footer-nav', [
'list-style' => 'none',
'padding' => '0',
'margin' => '0',
]);
$cssRules[] = $this->cssGenerator->generate('.roi-footer .footer-nav li', [
'margin-bottom' => $linkMb,
]);
$cssRules[] = $this->cssGenerator->generate('.roi-footer .footer-nav a', [
'color' => $linkColor,
'text-decoration' => 'none',
'transition' => "color {$transition}",
]);
$cssRules[] = $this->cssGenerator->generate('.roi-footer .footer-nav a:hover', [
'color' => $linkHoverColor,
]);
// Newsletter description
$cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-description', [
'color' => $textColor,
'margin-bottom' => '1rem',
'opacity' => '0.9',
]);
// Input newsletter
$cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-input', [
'width' => '100%',
'padding' => '0.75rem 1rem',
'background-color' => $inputBgColor,
'color' => $inputTextColor,
'border' => "1px solid {$inputBorderColor}",
'border-radius' => $inputRadius,
'margin-bottom' => '0.75rem',
]);
$cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-input:focus', [
'outline' => 'none',
'border-color' => $buttonBgColor,
'box-shadow' => "0 0 0 0.2rem rgba(13, 110, 253, 0.25)",
]);
// Boton newsletter
$cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-btn', [
'width' => '100%',
'padding' => '0.75rem 1.5rem',
'background-color' => $buttonBgColor,
'color' => $buttonTextColor,
'border' => 'none',
'border-radius' => $buttonRadius,
'font-weight' => '500',
'cursor' => 'pointer',
'transition' => "background-color {$transition}",
]);
$cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-btn:hover', [
'background-color' => $buttonHoverBg,
]);
$cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-btn:disabled', [
'opacity' => '0.7',
'cursor' => 'not-allowed',
]);
// Mensaje newsletter
$cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-message', [
'margin-top' => '0.75rem',
'padding' => '0.5rem',
'border-radius' => '4px',
'font-size' => '0.875rem',
'display' => 'none',
]);
$cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-message.success', [
'background-color' => '#d1e7dd',
'color' => '#0f5132',
]);
$cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-message.error', [
'background-color' => '#f8d7da',
'color' => '#842029',
]);
// Footer bottom (copyright)
$cssRules[] = $this->cssGenerator->generate('.roi-footer .footer-bottom', [
'border-top' => "1px solid {$borderTopColor}",
'padding-top' => $copyrightPy,
'margin-top' => '2rem',
'text-align' => 'center',
]);
$cssRules[] = $this->cssGenerator->generate('.roi-footer .copyright-text', [
'margin' => '0',
'opacity' => '0.9',
]);
// Responsive visibility
if (!$showDesktop) {
$cssRules[] = "@media (min-width: 992px) { .roi-footer { display: none !important; } }";
}
if (!$showMobile) {
$cssRules[] = "@media (max-width: 991px) { .roi-footer { display: none !important; } }";
}
return '<style>' . implode("\n", $cssRules) . '</style>';
}
private function generateHTML(array $data): string
{
$widget1 = $data['widget_1'] ?? [];
$widget2 = $data['widget_2'] ?? [];
$widget3 = $data['widget_3'] ?? [];
$newsletter = $data['newsletter'] ?? [];
$footerBottom = $data['footer_bottom'] ?? [];
$widget1Visible = $this->toBool($widget1['widget_1_visible'] ?? true);
$widget2Visible = $this->toBool($widget2['widget_2_visible'] ?? true);
$widget3Visible = $this->toBool($widget3['widget_3_visible'] ?? true);
$newsletterVisible = $this->toBool($newsletter['newsletter_visible'] ?? true);
$widget1Title = esc_html($widget1['widget_1_title'] ?? 'Recursos');
$widget2Title = esc_html($widget2['widget_2_title'] ?? 'Soporte');
$widget3Title = esc_html($widget3['widget_3_title'] ?? 'Empresa');
$newsletterTitle = esc_html($newsletter['newsletter_title'] ?? 'Suscribete al Newsletter');
$newsletterDesc = esc_html($newsletter['newsletter_description'] ?? 'Recibe las ultimas actualizaciones.');
$newsletterPlaceholder = esc_attr($newsletter['newsletter_placeholder'] ?? 'Email');
$newsletterBtnText = esc_html($newsletter['newsletter_button_text'] ?? 'Suscribirse');
$copyrightText = esc_html($footerBottom['copyright_text'] ?? date('Y') . ' Todos los derechos reservados.');
$nonce = wp_create_nonce(self::NONCE_ACTION);
$ajaxUrl = admin_url('admin-ajax.php');
$html = '<footer class="roi-footer">';
$html .= '<div class="container">';
$html .= '<div class="footer-grid">';
// Widget 1
if ($widget1Visible) {
$html .= '<div class="footer-widget footer-widget-menu">';
$html .= '<h5 class="widget-title">' . $widget1Title . '</h5>';
$html .= $this->renderMenu('footer_menu_1');
$html .= '</div>';
}
// Widget 2
if ($widget2Visible) {
$html .= '<div class="footer-widget footer-widget-menu">';
$html .= '<h5 class="widget-title">' . $widget2Title . '</h5>';
$html .= $this->renderMenu('footer_menu_2');
$html .= '</div>';
}
// Widget 3
if ($widget3Visible) {
$html .= '<div class="footer-widget footer-widget-menu">';
$html .= '<h5 class="widget-title">' . $widget3Title . '</h5>';
$html .= $this->renderMenu('footer_menu_3');
$html .= '</div>';
}
// Widget Newsletter
if ($newsletterVisible) {
$html .= '<div class="footer-widget footer-widget-newsletter">';
$html .= '<h5 class="widget-title">' . $newsletterTitle . '</h5>';
$html .= '<p class="newsletter-description">' . $newsletterDesc . '</p>';
$html .= '<form id="roi-newsletter-form" class="newsletter-form">';
$html .= '<input type="hidden" name="action" value="roi_newsletter_subscribe">';
$html .= '<input type="hidden" name="nonce" value="' . esc_attr($nonce) . '">';
$html .= '<input type="email" name="email" class="newsletter-input" placeholder="' . $newsletterPlaceholder . '" required>';
$html .= '<button type="submit" class="newsletter-btn">' . $newsletterBtnText . '</button>';
$html .= '<div class="newsletter-message"></div>';
$html .= '</form>';
$html .= '</div>';
}
$html .= '</div>'; // .footer-grid
// Footer bottom
$html .= '<div class="footer-bottom">';
$html .= '<p class="copyright-text">&copy; ' . $copyrightText . '</p>';
$html .= '</div>';
$html .= '</div>'; // .container
$html .= '</footer>';
return $html;
}
private function renderMenu(string $menuLocation): string
{
if (!has_nav_menu($menuLocation)) {
return '<p class="text-muted">Menu no asignado</p>';
}
return wp_nav_menu([
'theme_location' => $menuLocation,
'container' => false,
'menu_class' => 'footer-nav',
'fallback_cb' => false,
'echo' => false,
'depth' => 1,
]) ?: '';
}
private function generateJS(array $data): string
{
$newsletter = $data['newsletter'] ?? [];
$successMsg = esc_js($newsletter['newsletter_success_message'] ?? 'Gracias por suscribirte!');
$errorMsg = esc_js($newsletter['newsletter_error_message'] ?? 'Error al suscribirse. Intenta de nuevo.');
$ajaxUrl = admin_url('admin-ajax.php');
$js = <<<JS
<script>
(function() {
const form = document.getElementById('roi-newsletter-form');
if (!form) return;
form.addEventListener('submit', async function(e) {
e.preventDefault();
const btn = form.querySelector('.newsletter-btn');
const msgDiv = form.querySelector('.newsletter-message');
const emailInput = form.querySelector('input[name="email"]');
const originalText = btn.textContent;
// Reset message
msgDiv.style.display = 'none';
msgDiv.className = 'newsletter-message';
// Validate email
if (!emailInput.value || !emailInput.validity.valid) {
msgDiv.textContent = 'Por favor ingresa un email valido';
msgDiv.classList.add('error');
msgDiv.style.display = 'block';
return;
}
// Disable button
btn.disabled = true;
btn.textContent = 'Enviando...';
try {
const formData = new FormData(form);
const response = await fetch('{$ajaxUrl}', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
msgDiv.textContent = '{$successMsg}';
msgDiv.classList.add('success');
emailInput.value = '';
} else {
msgDiv.textContent = result.data?.message || '{$errorMsg}';
msgDiv.classList.add('error');
}
} catch (error) {
msgDiv.textContent = '{$errorMsg}';
msgDiv.classList.add('error');
}
msgDiv.style.display = 'block';
btn.disabled = false;
btn.textContent = originalText;
});
})();
</script>
JS;
return $js;
}
private function toBool($value): bool
{
return $value === true || $value === '1' || $value === 1;
}
}

View File

@@ -0,0 +1,278 @@
<?php
declare(strict_types=1);
namespace ROITheme\Public\Hero\Infrastructure\Ui;
use ROITheme\Shared\Domain\Contracts\RendererInterface;
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
use ROITheme\Shared\Domain\Entities\Component;
/**
* Class HeroRenderer
*
* Renderizador del componente Hero para el frontend.
*
* Responsabilidades:
* - Renderizar HTML del hero section con título del post/página
* - Mostrar badges de categorías (dinámicos desde WordPress)
* - Delegar generación de CSS a CSSGeneratorInterface
* - Validar visibilidad (is_enabled, show_on_pages, responsive)
* - Manejar visibilidad responsive con clases Bootstrap
*
* NO responsable de:
* - Generar string CSS (delega a CSSGeneratorService)
* - Persistir datos
* - Lógica de negocio
*
* @package ROITheme\Public\Hero\Infrastructure\Ui
*/
final class HeroRenderer implements RendererInterface
{
public function __construct(
private CSSGeneratorInterface $cssGenerator
) {}
public function render(Component $component): string
{
$data = $component->getData();
if (!$this->isEnabled($data)) {
return '';
}
if (!$this->shouldShowOnCurrentPage($data)) {
return '';
}
$css = $this->generateCSS($data);
$html = $this->buildHTML($data);
return sprintf("<style>%s</style>\n%s", $css, $html);
}
public function supports(string $componentType): bool
{
return $componentType === 'hero';
}
private function isEnabled(array $data): bool
{
return ($data['visibility']['is_enabled'] ?? false) === true;
}
private function shouldShowOnCurrentPage(array $data): bool
{
$showOn = $data['visibility']['show_on_pages'] ?? 'posts';
switch ($showOn) {
case 'all':
return true;
case 'home':
return is_front_page() || is_home();
case 'posts':
return is_single();
case 'pages':
return is_page();
default:
return true;
}
}
private function generateCSS(array $data): string
{
$colors = $data['colors'] ?? [];
$typography = $data['typography'] ?? [];
$spacing = $data['spacing'] ?? [];
$effects = $data['visual_effects'] ?? [];
$visibility = $data['visibility'] ?? [];
$gradientStart = $colors['gradient_start'] ?? '#1e3a5f';
$gradientEnd = $colors['gradient_end'] ?? '#2c5282';
$titleColor = $colors['title_color'] ?? '#FFFFFF';
$badgeBgColor = $colors['badge_bg_color'] ?? '#FFFFFF';
$badgeTextColor = $colors['badge_text_color'] ?? '#FFFFFF';
$badgeIconColor = $colors['badge_icon_color'] ?? '#FFB800';
$badgeHoverBg = $colors['badge_hover_bg'] ?? '#FF8600';
$titleFontSize = $typography['title_font_size'] ?? '2.5rem';
$titleFontSizeMobile = $typography['title_font_size_mobile'] ?? '1.75rem';
$titleFontWeight = $typography['title_font_weight'] ?? '700';
$titleLineHeight = $typography['title_line_height'] ?? '1.4';
$badgeFontSize = $typography['badge_font_size'] ?? '0.813rem';
$paddingVertical = $spacing['padding_vertical'] ?? '3rem';
$marginBottom = $spacing['margin_bottom'] ?? '1.5rem';
$badgePadding = $spacing['badge_padding'] ?? '0.375rem 0.875rem';
$badgeBorderRadius = $spacing['badge_border_radius'] ?? '20px';
$boxShadow = $effects['box_shadow'] ?? '0 4px 16px rgba(30, 58, 95, 0.25)';
$titleTextShadow = $effects['title_text_shadow'] ?? '1px 1px 2px rgba(0, 0, 0, 0.2)';
$badgeBackdropBlur = $effects['badge_backdrop_blur'] ?? '10px';
$showOnDesktop = $visibility['show_on_desktop'] ?? true;
$showOnMobile = $visibility['show_on_mobile'] ?? true;
$cssRules = [];
$cssRules[] = $this->cssGenerator->generate('.hero-section', [
'background' => "linear-gradient(135deg, {$gradientStart} 0%, {$gradientEnd} 100%)",
'box-shadow' => $boxShadow,
'padding' => "{$paddingVertical} 0",
'margin-bottom' => $marginBottom,
]);
$cssRules[] = $this->cssGenerator->generate('.hero-section__title', [
'color' => "{$titleColor} !important",
'font-weight' => $titleFontWeight,
'font-size' => $titleFontSize,
'line-height' => $titleLineHeight,
'text-shadow' => $titleTextShadow,
'margin-bottom' => '0',
'text-align' => 'center',
]);
$cssRules[] = $this->cssGenerator->generate('.hero-section__badge', [
'background' => $this->hexToRgba($badgeBgColor, 0.15),
'backdrop-filter' => "blur({$badgeBackdropBlur})",
'-webkit-backdrop-filter' => "blur({$badgeBackdropBlur})",
'border' => '1px solid ' . $this->hexToRgba($badgeBgColor, 0.2),
'color' => $this->hexToRgba($badgeTextColor, 0.95),
'padding' => $badgePadding,
'border-radius' => $badgeBorderRadius,
'font-size' => $badgeFontSize,
'font-weight' => '500',
'text-decoration' => 'none',
'display' => 'inline-block',
'transition' => 'all 0.3s ease',
]);
$cssRules[] = $this->cssGenerator->generate('.hero-section__badge:hover', [
'background' => $this->hexToRgba($badgeHoverBg, 0.2),
'border-color' => $this->hexToRgba($badgeHoverBg, 0.4),
'color' => '#ffffff',
]);
$cssRules[] = $this->cssGenerator->generate('.hero-section__badge i', [
'color' => $badgeIconColor,
]);
$cssRules[] = "@media (max-width: 767.98px) {
.hero-section__title {
font-size: {$titleFontSizeMobile};
}
}";
if (!$showOnMobile) {
$cssRules[] = "@media (max-width: 767.98px) {
.hero-section { display: none !important; }
}";
}
if (!$showOnDesktop) {
$cssRules[] = "@media (min-width: 768px) {
.hero-section { display: none !important; }
}";
}
return implode("\n", $cssRules);
}
private function buildHTML(array $data): string
{
$content = $data['content'] ?? [];
$showCategories = $content['show_categories'] ?? true;
$showBadgeIcon = $content['show_badge_icon'] ?? true;
$badgeIconClass = $content['badge_icon_class'] ?? 'bi-folder-fill';
$titleTag = $content['title_tag'] ?? 'h1';
$allowedTags = ['h1', 'h2', 'div'];
if (!in_array($titleTag, $allowedTags, true)) {
$titleTag = 'h1';
}
$title = is_singular() ? get_the_title() : '';
if (empty($title)) {
$title = wp_title('', false);
}
$html = '<div class="container-fluid hero-section">';
$html .= '<div class="container">';
if ($showCategories && is_single()) {
$categories = get_the_category();
if (!empty($categories)) {
$html .= '<div class="mb-3 d-flex justify-content-center">';
$html .= '<div class="d-flex gap-2 flex-wrap justify-content-center">';
foreach ($categories as $category) {
$categoryLink = esc_url(get_category_link($category->term_id));
$categoryName = esc_html($category->name);
$iconHtml = $showBadgeIcon
? '<i class="bi ' . esc_attr($badgeIconClass) . ' me-1"></i>'
: '';
$html .= sprintf(
'<a href="%s" class="hero-section__badge">%s%s</a>',
$categoryLink,
$iconHtml,
$categoryName
);
}
$html .= '</div>';
$html .= '</div>';
}
}
$html .= sprintf(
'<%s class="hero-section__title">%s</%s>',
$titleTag,
esc_html($title),
$titleTag
);
$html .= '</div>';
$html .= '</div>';
return $html;
}
private function hexToRgba(string $hex, float $alpha): string
{
$hex = ltrim($hex, '#');
if (strlen($hex) === 3) {
$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
}
$r = hexdec(substr($hex, 0, 2));
$g = hexdec(substr($hex, 2, 2));
$b = hexdec(substr($hex, 4, 2));
return "rgba({$r}, {$g}, {$b}, {$alpha})";
}
/**
* Genera clases Bootstrap de visibilidad responsive
*
* @param bool $desktop Si debe mostrarse en desktop
* @param bool $mobile Si debe mostrarse en mobile
* @return string|null Clases Bootstrap o null si visible en todos
*/
private function getVisibilityClasses(bool $desktop, bool $mobile): ?string
{
if ($desktop && $mobile) {
return null;
}
if ($desktop && !$mobile) {
return 'd-none d-md-block';
}
if (!$desktop && $mobile) {
return 'd-block d-md-none';
}
return 'd-none';
}
}

View File

@@ -1,11 +1,24 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace ROITheme\HeroSection\Infrastructure\Presentation\Public; namespace ROITheme\Public\herosection\infrastructure\ui;
use ROITheme\Component\Domain\Component; use ROITheme\Shared\Domain\Entities\Component;
use ROITheme\Component\Domain\RendererInterface; use ROITheme\Shared\Domain\Contracts\RendererInterface;
/**
* HeroSectionRenderer - Renderiza la sección hero con badges y título
*
* RESPONSABILIDAD: Generar HTML de la sección hero
*
* CARACTERÍSTICAS:
* - Badges de categorías con múltiples fuentes de datos
* - Título H1 con gradiente opcional
* - Múltiples tipos de fondo (color, gradiente, imagen)
* - Lógica condicional de visibilidad por tipo de página
*
* @package ROITheme\Public\HeroSection\Presentation
*/
final class HeroSectionRenderer implements RendererInterface final class HeroSectionRenderer implements RendererInterface
{ {
public function render(Component $component): string public function render(Component $component): string

View File

@@ -0,0 +1,364 @@
<?php
declare(strict_types=1);
namespace ROITheme\Public\Navbar\Infrastructure\Ui;
use ROITheme\Shared\Domain\Entities\Component;
use ROITheme\Shared\Domain\Contracts\RendererInterface;
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
use Walker_Nav_Menu;
/**
* NavbarRenderer - Renderiza el menú de navegación principal
*
* RESPONSABILIDAD: Generar HTML del menú de navegación WordPress
*
* CARACTERÍSTICAS:
* - Integración con wp_nav_menu()
* - Walker personalizado para Bootstrap 5
* - Soporte para submenús desplegables
* - Responsive con navbar-toggler
*
* Cumple con:
* - DIP: Recibe CSSGeneratorInterface por constructor
* - SRP: Una responsabilidad (renderizar navbar)
* - Clean Architecture: Infrastructure puede usar WordPress
*
* @package ROITheme\Public\Navbar\Infrastructure\Ui
*/
final class NavbarRenderer implements RendererInterface
{
/**
* @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS
*/
public function __construct(
private CSSGeneratorInterface $cssGenerator
) {}
public function render(Component $component): string
{
$data = $component->getData();
if (!$this->isEnabled($data)) {
return '';
}
$css = $this->generateCSS($data);
$html = $this->buildMenu($data);
return sprintf(
"<style>%s</style>\n%s",
$css,
$html
);
}
private function isEnabled(array $data): bool
{
return isset($data['visibility']['is_enabled']) &&
$data['visibility']['is_enabled'] === true;
}
private function shouldShowOnMobile(array $data): bool
{
return isset($data['visibility']['show_on_mobile']) &&
$data['visibility']['show_on_mobile'] === true;
}
/**
* Generar CSS usando CSSGeneratorService
*
* @param array $data Datos del componente
* @return string CSS generado
*/
private function generateCSS(array $data): string
{
$css = '';
// Obtener valores de configuración
$stickyEnabled = $data['visibility']['sticky_enabled'] ?? true;
$paddingVertical = $data['layout']['padding_vertical'] ?? '0.75rem 0';
$zIndex = $data['layout']['z_index'] ?? '1030';
$bgColor = $data['colors']['background_color'] ?? '#1e3a5f';
$boxShadow = $data['colors']['box_shadow'] ?? '0 4px 12px rgba(30, 58, 95, 0.15)';
$linkTextColor = $data['links']['text_color'] ?? '#FFFFFF';
$linkHoverColor = $data['links']['hover_color'] ?? '#FF8600';
$linkActiveColor = $data['links']['active_color'] ?? '#FF8600';
$linkFontSize = $data['links']['font_size'] ?? '0.9rem';
$linkFontWeight = $data['links']['font_weight'] ?? '500';
$linkPadding = $data['links']['padding'] ?? '0.5rem 0.65rem';
$linkBorderRadius = $data['links']['border_radius'] ?? '4px';
$showUnderlineEffect = $data['links']['show_underline_effect'] ?? true;
$underlineColor = $data['links']['underline_color'] ?? '#FF8600';
// Estilos del navbar container
$navbarStyles = [
'background-color' => $bgColor . ' !important',
'box-shadow' => $boxShadow,
'padding' => $paddingVertical,
'transition' => 'all 0.3s ease',
];
if ($stickyEnabled) {
$navbarStyles['position'] = 'sticky';
$navbarStyles['top'] = '0';
$navbarStyles['z-index'] = $zIndex;
}
$css .= $this->cssGenerator->generate('.navbar', $navbarStyles);
// Efecto scrolled del navbar
$css .= "\n" . $this->cssGenerator->generate('.navbar.scrolled', [
'box-shadow' => '0 6px 20px rgba(30, 58, 95, 0.25)',
]);
// Estilos de los enlaces del navbar
$navLinkStyles = [
'color' => 'rgba(255, 255, 255, 0.9) !important',
'font-weight' => $linkFontWeight,
'position' => 'relative',
'padding' => $linkPadding . ' !important',
'transition' => 'all 0.3s ease',
'font-size' => $linkFontSize,
'white-space' => 'nowrap',
];
$css .= "\n" . $this->cssGenerator->generate('.navbar .nav-link', $navLinkStyles);
// Efecto de subrayado (::after pseudo-element)
if ($showUnderlineEffect) {
$css .= "\n.navbar .nav-link::after {";
$css .= "\n content: '';";
$css .= "\n position: absolute;";
$css .= "\n bottom: 0;";
$css .= "\n left: 50%;";
$css .= "\n transform: translateX(-50%) scaleX(0);";
$css .= "\n width: 80%;";
$css .= "\n height: 2px;";
$css .= "\n background: {$underlineColor};";
$css .= "\n transition: transform 0.3s ease;";
$css .= "\n}";
$css .= "\n.navbar .nav-link:hover::after {";
$css .= "\n transform: translateX(-50%) scaleX(1);";
$css .= "\n}";
}
// Estilos hover y focus de los enlaces
$navLinkHoverStyles = [
'color' => $linkHoverColor . ' !important',
'background-color' => 'rgba(255, 133, 0, 0.1)',
'border-radius' => $linkBorderRadius,
];
$css .= "\n" . $this->cssGenerator->generate('.navbar .nav-link:hover, .navbar .nav-link:focus', $navLinkHoverStyles);
// Estilos de enlaces activos
$navLinkActiveStyles = [
'color' => $linkActiveColor . ' !important',
];
$css .= "\n" . $this->cssGenerator->generate('.navbar .nav-link.active, .navbar .nav-item.current-menu-item > .nav-link', $navLinkActiveStyles);
// Estilos del dropdown menu
$dropdownMaxHeight = $data['visual_effects']['dropdown_max_height'] ?? '300px';
$dropdownStyles = [
'background' => $data['visual_effects']['background_color'] ?? '#ffffff',
'border' => 'none',
'box-shadow' => $data['visual_effects']['shadow'] ?? '0 8px 24px rgba(0, 0, 0, 0.12)',
'border-radius' => $data['visual_effects']['border_radius'] ?? '8px',
'padding' => '0.5rem 0',
'max-height' => $dropdownMaxHeight,
'overflow-y' => 'auto',
];
$css .= "\n" . $this->cssGenerator->generate('.navbar .dropdown-menu', $dropdownStyles);
// Hover en desktop para mostrar dropdown (sin necesidad de clic)
$css .= "\n@media (min-width: 992px) {";
$css .= "\n .navbar .dropdown:hover > .dropdown-menu {";
$css .= "\n display: block;";
$css .= "\n margin-top: 0;";
$css .= "\n }";
$css .= "\n .navbar .dropdown > .dropdown-toggle:active {";
$css .= "\n pointer-events: none;";
$css .= "\n }";
$css .= "\n}";
// Estilos de items del dropdown
$dropdownItemStyles = [
'color' => $data['visual_effects']['item_color'] ?? '#495057',
'padding' => $data['visual_effects']['item_padding'] ?? '0.625rem 1.25rem',
'transition' => 'all 0.3s ease',
'font-weight' => '500',
];
$css .= "\n" . $this->cssGenerator->generate('.navbar .dropdown-item', $dropdownItemStyles);
// Estilos hover de items del dropdown
$dropdownItemHoverStyles = [
'background-color' => $data['visual_effects']['item_hover_background'] ?? 'rgba(255, 133, 0, 0.1)',
'color' => $linkHoverColor,
];
$css .= "\n" . $this->cssGenerator->generate('.navbar .dropdown-item:hover, .navbar .dropdown-item:focus', $dropdownItemHoverStyles);
// Estilos del brand (texto)
$brandStyles = [
'color' => ($data['media']['brand_color'] ?? '#FFFFFF') . ' !important',
'font-weight' => '700',
'font-size' => $data['media']['brand_font_size'] ?? '1.5rem',
'transition' => 'color 0.3s ease',
];
$css .= "\n" . $this->cssGenerator->generate('.navbar .navbar-brand, .navbar .roi-navbar-brand', $brandStyles);
// Estilos hover del brand
$brandHoverStyles = [
'color' => ($data['media']['brand_hover_color'] ?? '#FF8600') . ' !important',
];
$css .= "\n" . $this->cssGenerator->generate('.navbar .navbar-brand:hover, .navbar .roi-navbar-brand:hover', $brandHoverStyles);
// Estilos del logo (imagen)
$logoStyles = [
'height' => $data['media']['logo_height'] ?? '40px',
'width' => 'auto',
];
$css .= "\n" . $this->cssGenerator->generate('.navbar .roi-navbar-logo', $logoStyles);
return $css;
}
private function buildMenu(array $data): string
{
$menuLocation = $data['behavior']['menu_location'] ?? 'primary';
$enableDropdowns = $data['behavior']['enable_dropdowns'] ?? true;
$mobileBreakpoint = $data['behavior']['mobile_breakpoint'] ?? 'lg';
$ulClass = 'navbar-nav mb-2 mb-lg-0';
$args = [
'theme_location' => $menuLocation === 'custom' ? '' : $menuLocation,
'menu' => $menuLocation === 'custom' ? ($data['behavior']['custom_menu_id'] ?? 0) : '',
'container' => false,
'menu_class' => $ulClass,
'fallback_cb' => '__return_false',
'items_wrap' => '<ul id="%1$s" class="%2$s">%3$s</ul>',
'depth' => $enableDropdowns ? 2 : 1,
'walker' => new ROI_Bootstrap_Nav_Walker()
];
ob_start();
wp_nav_menu($args);
return ob_get_clean();
}
/**
* Obtiene las clases CSS de Bootstrap para visibilidad responsive
*
* Implementa tabla de decisión según especificación:
* - Desktop Y Mobile = null (visible en ambos)
* - Solo Desktop = 'd-none d-lg-block'
* - Solo Mobile = 'd-lg-none'
* - Ninguno = 'd-none' (oculto)
*
* @param bool $desktop Mostrar en desktop
* @param bool $mobile Mostrar en mobile
* @return string|null Clases CSS o null si visible en ambos
*/
private function getVisibilityClasses(bool $desktop, bool $mobile): ?string
{
if ($desktop && $mobile) {
return null; // Sin clases = visible siempre
}
if ($desktop && !$mobile) {
return 'd-none d-lg-block';
}
if (!$desktop && $mobile) {
return 'd-lg-none';
}
return 'd-none';
}
public function supports(string $componentType): bool
{
return $componentType === 'navbar';
}
}
/**
* Custom Walker for Bootstrap 5 Navigation
*
* RESPONSABILIDAD: Adaptar wp_nav_menu() a Bootstrap 5
*
* CARACTERÍSTICAS:
* - Clases Bootstrap 5 (.nav-item, .nav-link, .dropdown)
* - Atributos data-bs-toggle para dropdowns
* - Soporte para current-menu-item
*/
class ROI_Bootstrap_Nav_Walker extends Walker_Nav_Menu
{
public function start_lvl(&$output, $depth = 0, $args = null)
{
$indent = str_repeat("\t", $depth);
$output .= "\n$indent<ul class=\"dropdown-menu\">\n";
}
public function start_el(&$output, $item, $depth = 0, $args = null, $id = 0)
{
$indent = ($depth) ? str_repeat("\t", $depth) : '';
$classes = empty($item->classes) ? [] : (array) $item->classes;
$classes[] = 'nav-item';
if ($args->walker->has_children) {
$classes[] = 'dropdown';
}
$class_names = join(' ', apply_filters('nav_menu_css_class', array_filter($classes), $item, $args, $depth));
$class_names = $class_names ? ' class="' . esc_attr($class_names) . '"' : '';
$id = apply_filters('nav_menu_item_id', 'menu-item-' . $item->ID, $item, $args, $depth);
$id = $id ? ' id="' . esc_attr($id) . '"' : '';
$output .= $indent . '<li' . $id . $class_names . '>';
$atts = [];
$atts['title'] = !empty($item->attr_title) ? $item->attr_title : '';
$atts['target'] = !empty($item->target) ? $item->target : '';
$atts['rel'] = !empty($item->xfn) ? $item->xfn : '';
$atts['href'] = !empty($item->url) ? $item->url : '';
if ($depth === 0) {
$atts['class'] = 'nav-link';
if ($args->walker->has_children) {
$atts['class'] .= ' dropdown-toggle';
$atts['data-bs-toggle'] = 'dropdown';
$atts['role'] = 'button';
$atts['aria-expanded'] = 'false';
}
} else {
$atts['class'] = 'dropdown-item';
}
if (in_array('current-menu-item', $classes)) {
$atts['class'] .= ' active';
}
$atts = apply_filters('nav_menu_link_attributes', $atts, $item, $args, $depth);
$attributes = '';
foreach ($atts as $attr => $value) {
if (!empty($value)) {
$value = ('href' === $attr) ? esc_url($value) : esc_attr($value);
$attributes .= ' ' . $attr . '="' . $value . '"';
}
}
$title = apply_filters('the_title', $item->title, $item->ID);
$title = apply_filters('nav_menu_item_title', $title, $item, $args, $depth);
$item_output = $args->before;
$item_output .= '<a' . $attributes . '>';
$item_output .= $args->link_before . $title . $args->link_after;
$item_output .= '</a>';
$item_output .= $args->after;
$output .= apply_filters('walker_nav_menu_start_el', $item_output, $item, $depth, $args);
}
}

View File

@@ -0,0 +1,380 @@
<?php
declare(strict_types=1);
namespace ROITheme\Public\RelatedPost\Infrastructure\Ui;
use ROITheme\Shared\Domain\Contracts\RendererInterface;
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
use ROITheme\Shared\Domain\Entities\Component;
/**
* RelatedPostRenderer - Renderiza seccion de posts relacionados
*
* RESPONSABILIDAD: Generar HTML y CSS del componente Related Posts
*
* CARACTERISTICAS:
* - Grid responsive de cards
* - Query dinamica de posts
* - Paginacion Bootstrap
* - Estilos 100% desde BD via CSSGenerator
*
* @package ROITheme\Public\RelatedPost\Infrastructure\Ui
*/
final class RelatedPostRenderer implements RendererInterface
{
public function __construct(
private CSSGeneratorInterface $cssGenerator
) {}
public function render(Component $component): string
{
$data = $component->getData();
if (!$this->isEnabled($data)) {
return '';
}
if (!$this->shouldShowOnCurrentPage($data)) {
return '';
}
$visibilityClass = $this->getVisibilityClass($data);
if ($visibilityClass === null) {
return '';
}
$css = $this->generateCSS($data);
$html = $this->buildHTML($data, $visibilityClass);
return sprintf("<style>%s</style>\n%s", $css, $html);
}
public function supports(string $componentType): bool
{
return $componentType === 'related-post';
}
private function isEnabled(array $data): bool
{
$value = $data['visibility']['is_enabled'] ?? false;
return $value === true || $value === '1' || $value === 1;
}
private function shouldShowOnCurrentPage(array $data): bool
{
$showOn = $data['visibility']['show_on_pages'] ?? 'posts';
switch ($showOn) {
case 'all':
return true;
case 'posts':
return is_single();
case 'pages':
return is_page();
default:
return true;
}
}
private function getVisibilityClass(array $data): ?string
{
$showDesktop = $data['visibility']['show_on_desktop'] ?? true;
$showDesktop = $showDesktop === true || $showDesktop === '1' || $showDesktop === 1;
$showMobile = $data['visibility']['show_on_mobile'] ?? true;
$showMobile = $showMobile === true || $showMobile === '1' || $showMobile === 1;
if (!$showDesktop && !$showMobile) {
return null;
}
if (!$showDesktop && $showMobile) {
return 'd-lg-none';
}
if ($showDesktop && !$showMobile) {
return 'd-none d-lg-block';
}
return '';
}
private function generateCSS(array $data): string
{
$colors = $data['colors'] ?? [];
$spacing = $data['spacing'] ?? [];
$effects = $data['visual_effects'] ?? [];
$typography = $data['typography'] ?? [];
$visibility = $data['visibility'] ?? [];
$cssRules = [];
// Variables de colores del tema (defaults del template)
$colorNavyPrimary = $colors['section_title_color'] ?? '#0E2337';
$colorOrangePrimary = $colors['card_hover_border_color'] ?? '#FF8600';
$colorNeutral50 = '#f9fafb';
$colorNeutral100 = '#e5e7eb';
$colorNeutral600 = $colors['card_border_color'] ?? '#6b7280';
// Container - margin 3rem 0
$cssRules[] = $this->cssGenerator->generate('.related-posts', [
'margin' => '3rem 0',
]);
// Section title - color navy, font-weight 700, margin-bottom 2rem
$cssRules[] = $this->cssGenerator->generate('.related-posts h2', [
'color' => $colorNavyPrimary,
'font-weight' => '700',
'margin-bottom' => '2rem',
]);
// Card styles - cursor pointer, border, border-left 4px
$cardBgColor = $colors['card_bg_color'] ?? '#ffffff';
$cardHoverBgColor = $colors['card_hover_bg_color'] ?? $colorNeutral50;
$cssRules[] = ".related-posts .card {
cursor: pointer;
background: {$cardBgColor} !important;
border: 1px solid {$colorNeutral100} !important;
border-left: 4px solid {$colorNeutral600} !important;
transition: all 0.3s ease;
height: 100%;
}";
// Card hover - background change, shadow, border-left orange
$cssRules[] = ".related-posts .card:hover {
background: {$cardHoverBgColor} !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1) !important;
border-left-color: {$colorOrangePrimary} !important;
}";
// Card body - padding 1.5rem
$cssRules[] = $this->cssGenerator->generate('.related-posts .card-body', [
'padding' => '1.5rem !important',
]);
// Card title - color navy, font-weight 600, font-size 0.95rem
$cardTitleColor = $colors['card_title_color'] ?? $colorNavyPrimary;
$cssRules[] = ".related-posts .card-title {
color: {$cardTitleColor} !important;
font-weight: 600;
font-size: 0.95rem;
line-height: 1.4;
}";
// Link hover - title changes to orange
$cssRules[] = ".related-posts a:hover .card-title {
color: {$colorOrangePrimary} !important;
}";
// Pagination styles - matching template exactly
$cssRules[] = ".related-posts .page-link {
color: {$colorNeutral600};
border: 1px solid {$colorNeutral100};
padding: 0.5rem 1rem;
margin: 0 0.25rem;
border-radius: 4px;
font-weight: 500;
transition: all 0.3s ease;
}";
$cssRules[] = ".related-posts .page-link:hover {
background-color: rgba(255, 133, 0, 0.1);
border-color: {$colorOrangePrimary};
color: {$colorOrangePrimary};
}";
$cssRules[] = ".related-posts .page-item.active .page-link {
background-color: {$colorOrangePrimary};
border-color: {$colorOrangePrimary};
color: #ffffff;
}";
// Responsive visibility
$showOnDesktop = $visibility['show_on_desktop'] ?? true;
$showOnDesktop = $showOnDesktop === true || $showOnDesktop === '1' || $showOnDesktop === 1;
$showOnMobile = $visibility['show_on_mobile'] ?? true;
$showOnMobile = $showOnMobile === true || $showOnMobile === '1' || $showOnMobile === 1;
if (!$showOnMobile) {
$cssRules[] = "@media (max-width: 991.98px) {
.related-posts { display: none !important; }
}";
}
if (!$showOnDesktop) {
$cssRules[] = "@media (min-width: 992px) {
.related-posts { display: none !important; }
}";
}
return implode("\n", $cssRules);
}
private function buildHTML(array $data, string $visibilityClass): string
{
$content = $data['content'] ?? [];
$layout = $data['layout'] ?? [];
$sectionTitle = $content['section_title'] ?? 'Descubre Mas Contenido';
$postsPerPage = (int)($content['posts_per_page'] ?? 12);
$orderby = $content['orderby'] ?? 'rand';
$order = $content['order'] ?? 'DESC';
$showPagination = $content['show_pagination'] ?? true;
$showPagination = $showPagination === true || $showPagination === '1' || $showPagination === 1;
// Layout columns (cast to string to handle boolean conversion from DB)
$colsDesktop = (string)($layout['columns_desktop'] ?? '3');
$colsTablet = (string)($layout['columns_tablet'] ?? '2');
$colsMobile = (string)($layout['columns_mobile'] ?? '1');
// Handle '1' stored as boolean true in DB
if ($colsDesktop === '1' || $colsDesktop === '') $colsDesktop = '3';
if ($colsTablet === '1' || $colsTablet === '') $colsTablet = '2';
if ($colsMobile === '1' || $colsMobile === '') $colsMobile = '1';
// Bootstrap column classes
$colClass = $this->getColumnClass($colsDesktop, $colsTablet, $colsMobile);
// Query related posts
$posts = $this->getRelatedPosts($postsPerPage, $orderby, $order);
if (empty($posts)) {
return '';
}
$containerClass = 'my-5 related-posts';
if (!empty($visibilityClass)) {
$containerClass .= ' ' . $visibilityClass;
}
$html = sprintf('<div class="%s">', esc_attr($containerClass));
$html .= sprintf(
'<h2 class="h3 mb-4">%s</h2>',
esc_html($sectionTitle)
);
$html .= '<div class="row g-4">';
foreach ($posts as $post) {
$html .= $this->buildCardHTML($post, $colClass);
}
$html .= '</div>';
if ($showPagination) {
$html .= $this->buildPaginationHTML($data);
}
$html .= '</div>';
// Reset post data
wp_reset_postdata();
return $html;
}
private function getColumnClass(string $desktop, string $tablet, string $mobile): string
{
$desktopCols = 12 / (int)$desktop;
$tabletCols = 12 / (int)$tablet;
$mobileCols = 12 / (int)$mobile;
// Template original usa col-md-4 (3 columnas desde tablet)
// col-{mobile} col-md-{tablet/desktop}
return sprintf('col-%d col-md-%d', $mobileCols, $desktopCols);
}
private function getRelatedPosts(int $perPage, string $orderby, string $order): array
{
$currentPostId = get_the_ID();
$args = [
'post_type' => 'post',
'posts_per_page' => $perPage,
'post__not_in' => $currentPostId ? [$currentPostId] : [],
'orderby' => $orderby,
'order' => $order,
'no_found_rows' => true,
];
$query = new \WP_Query($args);
return $query->posts;
}
private function buildCardHTML(\WP_Post $post, string $colClass): string
{
$permalink = get_permalink($post);
$title = get_the_title($post);
$html = sprintf('<div class="%s">', esc_attr($colClass));
$html .= sprintf(
'<a href="%s" class="text-decoration-none">',
esc_url($permalink)
);
$html .= '<div class="card h-100">';
$html .= '<div class="card-body d-flex align-items-center justify-content-center">';
$html .= sprintf(
'<h5 class="card-title h6 mb-0 text-center">%s</h5>',
esc_html($title)
);
$html .= '</div>';
$html .= '</div>';
$html .= '</a>';
$html .= '</div>';
return $html;
}
private function buildPaginationHTML(array $data): string
{
$content = $data['content'] ?? [];
$textFirst = $content['pagination_text_first'] ?? 'Inicio';
$textLast = $content['pagination_text_last'] ?? 'Fin';
$textMore = $content['pagination_text_more'] ?? 'Ver mas';
$html = '<nav aria-label="' . esc_attr__('Navegacion de posts relacionados', 'roi-theme') . '" class="mt-5">';
$html .= '<ul class="pagination justify-content-center">';
// First page
$html .= '<li class="page-item">';
$html .= sprintf(
'<a class="page-link" href="#" aria-label="%s">%s</a>',
esc_attr($textFirst),
esc_html($textFirst)
);
$html .= '</li>';
// Page numbers (static for now, can be enhanced with AJAX later)
for ($i = 1; $i <= 5; $i++) {
$activeClass = $i === 1 ? ' active' : '';
$ariaCurrent = $i === 1 ? ' aria-current="page"' : '';
$html .= sprintf(
'<li class="page-item%s"%s><a class="page-link" href="#">%d</a></li>',
$activeClass,
$ariaCurrent,
$i
);
}
// More link
$html .= '<li class="page-item">';
$html .= sprintf(
'<a class="page-link" href="#">%s</a>',
esc_html($textMore)
);
$html .= '</li>';
// Last page
$html .= '<li class="page-item">';
$html .= sprintf(
'<a class="page-link" href="#" aria-label="%s">%s</a>',
esc_attr($textLast),
esc_html($textLast)
);
$html .= '</li>';
$html .= '</ul>';
$html .= '</nav>';
return $html;
}
}

View File

@@ -0,0 +1,390 @@
<?php
declare(strict_types=1);
namespace ROITheme\Public\SocialShare\Infrastructure\Ui;
use ROITheme\Shared\Domain\Contracts\RendererInterface;
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
use ROITheme\Shared\Domain\Entities\Component;
/**
* SocialShareRenderer - Renderiza botones de compartir en redes sociales
*
* RESPONSABILIDAD: Generar HTML y CSS del componente Social Share
*
* CARACTERISTICAS:
* - 6 redes sociales: Facebook, Instagram, LinkedIn, WhatsApp, X, Email
* - Colores configurables por red
* - Toggle individual por red social
* - Estilos 100% desde BD via CSSGenerator
*
* Cumple con:
* - DIP: Recibe CSSGeneratorInterface por constructor
* - SRP: Una responsabilidad (renderizar social share)
* - Clean Architecture: Infrastructure puede usar WordPress
*
* @package ROITheme\Public\SocialShare\Infrastructure\Ui
*/
final class SocialShareRenderer implements RendererInterface
{
private const NETWORKS = [
'facebook' => [
'field' => 'show_facebook',
'url_field' => 'facebook_url',
'icon' => 'bi-facebook',
'label' => 'Facebook',
'share_pattern' => 'https://www.facebook.com/sharer/sharer.php?u=%s',
],
'instagram' => [
'field' => 'show_instagram',
'url_field' => 'instagram_url',
'icon' => 'bi-instagram',
'label' => 'Instagram',
'share_pattern' => '', // Instagram no tiene share directo - requiere URL configurada
],
'linkedin' => [
'field' => 'show_linkedin',
'url_field' => 'linkedin_url',
'icon' => 'bi-linkedin',
'label' => 'LinkedIn',
'share_pattern' => 'https://www.linkedin.com/sharing/share-offsite/?url=%s',
],
'whatsapp' => [
'field' => 'show_whatsapp',
'url_field' => 'whatsapp_number',
'icon' => 'bi-whatsapp',
'label' => 'WhatsApp',
'share_pattern' => 'https://wa.me/?text=%s', // Compartir via WhatsApp
],
'twitter' => [
'field' => 'show_twitter',
'url_field' => 'twitter_url',
'icon' => 'bi-twitter-x',
'label' => 'X',
'share_pattern' => 'https://twitter.com/intent/tweet?url=%s&text=%s',
],
'email' => [
'field' => 'show_email',
'url_field' => 'email_address',
'icon' => 'bi-envelope',
'label' => 'Email',
'share_pattern' => 'mailto:?subject=%s&body=%s', // Compartir via Email
],
];
public function __construct(
private CSSGeneratorInterface $cssGenerator
) {}
public function render(Component $component): string
{
$data = $component->getData();
if (!$this->isEnabled($data)) {
return '';
}
if (!$this->shouldShowOnCurrentPage($data)) {
return '';
}
$css = $this->generateCSS($data);
$html = $this->buildHTML($data);
return sprintf("<style>%s</style>\n%s", $css, $html);
}
public function supports(string $componentType): bool
{
return $componentType === 'social-share';
}
private function isEnabled(array $data): bool
{
$value = $data['visibility']['is_enabled'] ?? false;
return $value === true || $value === '1' || $value === 1;
}
private function shouldShowOnCurrentPage(array $data): bool
{
$showOn = $data['visibility']['show_on_pages'] ?? 'posts';
switch ($showOn) {
case 'all':
return true;
case 'posts':
return is_single();
case 'pages':
return is_page();
default:
return true;
}
}
private function generateCSS(array $data): string
{
$colors = $data['colors'] ?? [];
$spacing = $data['spacing'] ?? [];
$typography = $data['typography'] ?? [];
$effects = $data['visual_effects'] ?? [];
$visibility = $data['visibility'] ?? [];
$cssRules = [];
$transitionDuration = $effects['transition_duration'] ?? '0.3s';
$buttonBorderWidth = $effects['button_border_width'] ?? '2px';
// Container styles
$cssRules[] = $this->cssGenerator->generate('.social-share-container', [
'margin-top' => $spacing['container_margin_top'] ?? '3rem',
'margin-bottom' => $spacing['container_margin_bottom'] ?? '3rem',
'padding-top' => $spacing['container_padding_top'] ?? '1.5rem',
'padding-bottom' => $spacing['container_padding_bottom'] ?? '1.5rem',
'border-top' => sprintf('%s solid %s',
$effects['border_top_width'] ?? '1px',
$colors['border_top_color'] ?? '#dee2e6'
),
]);
// Label styles
$cssRules[] = $this->cssGenerator->generate('.social-share-container .share-label', [
'font-size' => $typography['label_font_size'] ?? '1rem',
'color' => $colors['label_color'] ?? '#6c757d',
'margin-bottom' => $spacing['label_margin_bottom'] ?? '1rem',
]);
// Buttons wrapper
$cssRules[] = $this->cssGenerator->generate('.social-share-container .share-buttons', [
'display' => 'flex',
'flex-wrap' => 'wrap',
'gap' => $spacing['buttons_gap'] ?? '0.5rem',
]);
// Base button styles
$cssRules[] = $this->cssGenerator->generate('.social-share-container .share-buttons .btn', [
'padding' => $spacing['button_padding'] ?? '0.25rem 0.5rem',
'font-size' => $typography['icon_font_size'] ?? '1rem',
'border-width' => $buttonBorderWidth,
'border-radius' => $effects['button_border_radius'] ?? '0.375rem',
'transition' => "all {$transitionDuration} ease",
'background-color' => $colors['button_background'] ?? '#ffffff',
]);
// Hover effect
$cssRules[] = $this->cssGenerator->generate('.social-share-container .share-buttons .btn:hover', [
'box-shadow' => $effects['hover_box_shadow'] ?? '0 4px 12px rgba(0, 0, 0, 0.15)',
]);
// Network-specific colors
$networkColors = [
'facebook' => $colors['facebook_color'] ?? '#0d6efd',
'instagram' => $colors['instagram_color'] ?? '#dc3545',
'linkedin' => $colors['linkedin_color'] ?? '#0dcaf0',
'whatsapp' => $colors['whatsapp_color'] ?? '#198754',
'twitter' => $colors['twitter_color'] ?? '#212529',
'email' => $colors['email_color'] ?? '#6c757d',
];
foreach ($networkColors as $network => $color) {
// Outline style
$cssRules[] = $this->cssGenerator->generate(".social-share-container .btn-share-{$network}", [
'color' => $color,
'border-color' => $color,
]);
// Hover fills the button
$cssRules[] = $this->cssGenerator->generate(".social-share-container .btn-share-{$network}:hover", [
'background-color' => $color,
'color' => '#ffffff',
]);
}
// Responsive visibility (normalizar booleanos desde BD)
$showOnDesktop = $visibility['show_on_desktop'] ?? true;
$showOnDesktop = $showOnDesktop === true || $showOnDesktop === '1' || $showOnDesktop === 1;
$showOnMobile = $visibility['show_on_mobile'] ?? true;
$showOnMobile = $showOnMobile === true || $showOnMobile === '1' || $showOnMobile === 1;
if (!$showOnMobile) {
$cssRules[] = "@media (max-width: 991.98px) {
.social-share-container { display: none !important; }
}";
}
if (!$showOnDesktop) {
$cssRules[] = "@media (min-width: 992px) {
.social-share-container { display: none !important; }
}";
}
return implode("\n", $cssRules);
}
private function buildHTML(array $data): string
{
$content = $data['content'] ?? [];
$networks = $data['networks'] ?? [];
$labelText = $content['label_text'] ?? 'Compartir:';
$showLabel = $content['show_label'] ?? true;
$showLabel = $showLabel === true || $showLabel === '1' || $showLabel === 1;
$html = '<div class="social-share-container">';
// Label
if ($showLabel && !empty($labelText)) {
$html .= sprintf(
'<p class="share-label">%s</p>',
esc_html($labelText)
);
}
// Buttons wrapper
$html .= '<div class="share-buttons">';
// Get current post data for share URLs
$shareUrl = $this->getCurrentUrl();
$shareTitle = $this->getCurrentTitle();
foreach (self::NETWORKS as $networkKey => $networkData) {
$fieldKey = $networkData['field'];
$isEnabled = $networks[$fieldKey] ?? true;
$isEnabled = $isEnabled === true || $isEnabled === '1' || $isEnabled === 1;
if (!$isEnabled) {
continue;
}
// Obtener URL configurada para esta red
$urlFieldKey = $networkData['url_field'];
$configuredUrl = $networks[$urlFieldKey] ?? '';
$shareHref = $this->buildNetworkUrl(
$networkKey,
$configuredUrl,
$networkData['share_pattern'],
$shareUrl,
$shareTitle
);
// Si no hay URL válida usar "#" como fallback (para mantener el icono visible)
if (empty($shareHref)) {
$shareHref = '#';
}
$ariaLabel = sprintf('Compartir en %s', $networkData['label']);
$html .= sprintf(
'<a href="%s" class="btn btn-share-%s" aria-label="%s" target="_blank" rel="noopener noreferrer">
<i class="bi %s"></i>
</a>',
esc_url($shareHref),
esc_attr($networkKey),
esc_attr($ariaLabel),
esc_attr($networkData['icon'])
);
}
$html .= '</div>'; // .share-buttons
$html .= '</div>'; // .social-share-container
return $html;
}
private function getCurrentUrl(): string
{
if (is_singular()) {
return get_permalink() ?: '';
}
return home_url(add_query_arg([], $GLOBALS['wp']->request ?? ''));
}
private function getCurrentTitle(): string
{
if (is_singular()) {
return get_the_title() ?: '';
}
return wp_title('', false) ?: get_bloginfo('name');
}
/**
* Construye la URL para un botón de red social
*
* Prioridad:
* 1. URL configurada por el usuario → enlace directo al perfil
* 2. Sin URL configurada → usar patrón de compartir (si existe)
*/
private function buildNetworkUrl(
string $network,
string $configuredUrl,
string $sharePattern,
string $pageUrl,
string $pageTitle
): string {
// Si hay URL configurada, usarla directamente
if (!empty($configuredUrl)) {
return $this->formatConfiguredUrl($network, $configuredUrl);
}
// Si no hay URL configurada pero existe patrón de compartir
if (!empty($sharePattern)) {
return $this->formatShareUrl($network, $sharePattern, $pageUrl, $pageTitle);
}
return '#';
}
/**
* Formatea URL configurada según el tipo de red
*/
private function formatConfiguredUrl(string $network, string $url): string
{
switch ($network) {
case 'whatsapp':
// Para WhatsApp, el número debe ir sin el +
$number = preg_replace('/[^0-9]/', '', $url);
return "https://wa.me/{$number}";
case 'email':
// Para email, agregar mailto: si no lo tiene
if (!str_starts_with($url, 'mailto:')) {
return "mailto:{$url}";
}
return $url;
default:
return $url;
}
}
/**
* Formatea URL de compartir usando el patrón
*/
private function formatShareUrl(string $network, string $pattern, string $url, string $title): string
{
$encodedUrl = rawurlencode($url);
$encodedTitle = rawurlencode($title);
switch ($network) {
case 'twitter':
return sprintf($pattern, $encodedUrl, $encodedTitle);
case 'whatsapp':
$text = $title . ' - ' . $url;
return sprintf($pattern, rawurlencode($text));
case 'email':
return sprintf($pattern, $encodedTitle, $encodedUrl);
default:
return sprintf($pattern, $encodedUrl);
}
}
private function getVisibilityClasses(bool $desktop, bool $mobile): ?string
{
if (!$desktop && !$mobile) {
return null;
}
if (!$desktop && $mobile) {
return 'd-lg-none';
}
if ($desktop && !$mobile) {
return 'd-none d-lg-block';
}
return '';
}
}

View File

@@ -0,0 +1,491 @@
<?php
declare(strict_types=1);
namespace ROITheme\Public\TableOfContents\Infrastructure\Ui;
use ROITheme\Shared\Domain\Contracts\RendererInterface;
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
use ROITheme\Shared\Domain\Entities\Component;
use DOMDocument;
use DOMXPath;
/**
* TableOfContentsRenderer - Renderiza tabla de contenido con navegacion automatica
*
* RESPONSABILIDAD: Generar HTML y CSS de la tabla de contenido
*
* CARACTERISTICAS:
* - Generacion automatica desde headings del contenido
* - ScrollSpy para navegacion activa
* - Sticky positioning configurable
* - Smooth scroll
* - Estilos 100% desde BD via CSSGenerator
*
* Cumple con:
* - DIP: Recibe CSSGeneratorInterface por constructor
* - SRP: Una responsabilidad (renderizar TOC)
* - Clean Architecture: Infrastructure puede usar WordPress
*
* @package ROITheme\Public\TableOfContents\Infrastructure\Ui
*/
final class TableOfContentsRenderer implements RendererInterface
{
private array $headingCounter = [];
public function __construct(
private CSSGeneratorInterface $cssGenerator
) {}
public function render(Component $component): string
{
$data = $component->getData();
if (!$this->isEnabled($data)) {
return '';
}
if (!$this->shouldShowOnCurrentPage($data)) {
return '';
}
$tocItems = $this->generateTocItems($data);
if (empty($tocItems)) {
return '';
}
$css = $this->generateCSS($data);
$html = $this->buildHTML($data, $tocItems);
$script = $this->buildScript($data);
return sprintf("<style>%s</style>\n%s\n%s", $css, $html, $script);
}
public function supports(string $componentType): bool
{
return $componentType === 'table-of-contents';
}
private function isEnabled(array $data): bool
{
return ($data['visibility']['is_enabled'] ?? false) === true;
}
private function shouldShowOnCurrentPage(array $data): bool
{
$showOn = $data['visibility']['show_on_pages'] ?? 'posts';
switch ($showOn) {
case 'all':
return true;
case 'posts':
return is_single();
case 'pages':
return is_page();
default:
return true;
}
}
private function getVisibilityClasses(bool $desktop, bool $mobile): ?string
{
if (!$desktop && !$mobile) {
return null;
}
if (!$desktop && $mobile) {
return 'd-lg-none';
}
if ($desktop && !$mobile) {
return 'd-none d-lg-block';
}
return '';
}
private function generateTocItems(array $data): array
{
$content = $data['content'] ?? [];
$autoGenerate = $content['auto_generate'] ?? true;
if (!$autoGenerate) {
return [];
}
$headingLevelsStr = $content['heading_levels'] ?? 'h2,h3';
$headingLevels = array_map('trim', explode(',', $headingLevelsStr));
return $this->generateTocFromContent($headingLevels);
}
private function generateTocFromContent(array $headingLevels): array
{
global $post;
if (!$post || empty($post->post_content)) {
return [];
}
$content = apply_filters('the_content', $post->post_content);
$dom = new DOMDocument();
libxml_use_internal_errors(true);
$dom->loadHTML('<?xml encoding="utf-8" ?>' . $content);
libxml_clear_errors();
$xpath = new DOMXPath($dom);
$tocItems = [];
$xpathQuery = implode(' | ', array_map(function($level) {
return '//' . $level;
}, $headingLevels));
$headings = $xpath->query($xpathQuery);
if ($headings->length === 0) {
return [];
}
foreach ($headings as $heading) {
$tagName = strtolower($heading->tagName);
$level = intval(substr($tagName, 1));
$text = trim($heading->textContent);
if (empty($text)) {
continue;
}
$existingId = $heading->getAttribute('id');
if (empty($existingId)) {
$anchor = $this->generateAnchorId($text);
$this->addIdToHeading($text, $anchor);
} else {
$anchor = $existingId;
}
$tocItems[] = [
'text' => $text,
'anchor' => $anchor,
'level' => $level
];
}
return $tocItems;
}
private function generateAnchorId(string $text): string
{
$id = strtolower($text);
$id = remove_accents($id);
$id = preg_replace('/[^a-z0-9]+/', '-', $id);
$id = trim($id, '-');
$baseId = $id;
$count = 1;
while (isset($this->headingCounter[$id])) {
$id = $baseId . '-' . $count;
$count++;
}
$this->headingCounter[$id] = true;
return $id;
}
private function addIdToHeading(string $headingText, string $anchorId): void
{
add_filter('the_content', function($content) use ($headingText, $anchorId) {
$pattern = '/<(h[2-6])([^>]*)>(\s*)' . preg_quote($headingText, '/') . '(\s*)<\/\1>/i';
$replacement = '<$1 id="' . esc_attr($anchorId) . '"$2>$3' . $headingText . '$4</$1>';
return preg_replace($pattern, $replacement, $content, 1);
}, 20);
}
private function generateCSS(array $data): string
{
$colors = $data['colors'] ?? [];
$spacing = $data['spacing'] ?? [];
$typography = $data['typography'] ?? [];
$effects = $data['visual_effects'] ?? [];
$behavior = $data['behavior'] ?? [];
$visibility = $data['visibility'] ?? [];
$cssRules = [];
// Container styles - Flexbox layout for proper scrolling
$cssRules[] = $this->cssGenerator->generate('.toc-container', [
'background-color' => $colors['background_color'] ?? '#ffffff',
'border' => ($effects['border_width'] ?? '1px') . ' solid ' . ($colors['border_color'] ?? '#E6E9ED'),
'border-radius' => $effects['border_radius'] ?? '8px',
'box-shadow' => $effects['box_shadow'] ?? '0 2px 8px rgba(0, 0, 0, 0.08)',
'padding' => $spacing['container_padding'] ?? '12px 16px',
'margin-bottom' => $spacing['margin_bottom'] ?? '13px',
'max-height' => $behavior['max_height'] ?? 'calc(100vh - 71px - 10px - 250px - 15px - 15px)',
'display' => 'flex',
'flex-direction' => 'column',
'overflow' => 'visible',
]);
// Sticky behavior - aplica al wrapper .sidebar-sticky de single.php
// NO al .toc-container individual (ver template líneas 817-835)
if (($behavior['is_sticky'] ?? true)) {
$cssRules[] = $this->cssGenerator->generate('.sidebar-sticky', [
'position' => 'sticky',
'top' => '85px',
'display' => 'flex',
'flex-direction' => 'column',
]);
}
// Custom scrollbar
$cssRules[] = $this->cssGenerator->generate('.toc-container::-webkit-scrollbar', [
'width' => $spacing['scrollbar_width'] ?? '6px',
]);
$cssRules[] = $this->cssGenerator->generate('.toc-container::-webkit-scrollbar-track', [
'background' => $colors['scrollbar_track_color'] ?? '#F9FAFB',
'border-radius' => $effects['scrollbar_border_radius'] ?? '3px',
]);
$cssRules[] = $this->cssGenerator->generate('.toc-container::-webkit-scrollbar-thumb', [
'background' => $colors['scrollbar_thumb_color'] ?? '#6B7280',
'border-radius' => $effects['scrollbar_border_radius'] ?? '3px',
]);
// Title styles - Color #1e3a5f = navy-primary del Design System
$cssRules[] = $this->cssGenerator->generate('.toc-container .toc-title', [
'font-size' => $typography['title_font_size'] ?? '1rem',
'font-weight' => $typography['title_font_weight'] ?? '600',
'color' => $colors['title_color'] ?? '#1e3a5f',
'padding-bottom' => $spacing['title_padding_bottom'] ?? '8px',
'margin-bottom' => $spacing['title_margin_bottom'] ?? '0.75rem',
'border-bottom' => '2px solid ' . ($colors['title_border_color'] ?? '#E6E9ED'),
'margin-top' => '0',
]);
// List styles - Scrollable area with flex
$cssRules[] = $this->cssGenerator->generate('.toc-container .toc-list', [
'margin' => '0',
'padding' => '0',
'padding-right' => '0.5rem',
'list-style' => 'none',
'overflow-y' => 'auto',
'flex' => '1',
'min-height' => '0',
]);
$cssRules[] = $this->cssGenerator->generate('.toc-container .toc-list li', [
'margin-bottom' => $spacing['item_margin_bottom'] ?? '0.15rem',
]);
// Link styles - Color #495057 = neutral-600 del template
$transitionDuration = $effects['transition_duration'] ?? '0.3s';
$cssRules[] = $this->cssGenerator->generate('.toc-container .toc-link', [
'display' => 'block',
'font-size' => $typography['link_font_size'] ?? '0.9rem',
'line-height' => $typography['link_line_height'] ?? '1.3',
'color' => $colors['link_color'] ?? '#495057',
'text-decoration' => 'none',
'padding' => $spacing['link_padding'] ?? '0.3rem 0.85rem',
'border-radius' => $effects['link_border_radius'] ?? '4px',
'border-left' => ($effects['active_border_left_width'] ?? '3px') . ' solid transparent',
'transition' => "all {$transitionDuration} ease",
]);
// Link hover - Color #1e3a5f = navy-primary del Design System
// Template: background, border-left-color, color
$cssRules[] = $this->cssGenerator->generate('.toc-container .toc-link:hover', [
'color' => $colors['link_hover_color'] ?? '#1e3a5f',
'background-color' => $colors['link_hover_background'] ?? '#F9FAFB',
'border-left-color' => $colors['active_border_color'] ?? '#1e3a5f',
]);
// Active link - Color #1e3a5f = navy-primary del Design System
// Template: font-weight: 600
$cssRules[] = $this->cssGenerator->generate('.toc-container .toc-link.active', [
'color' => $colors['active_text_color'] ?? '#1e3a5f',
'background-color' => $colors['active_background_color'] ?? '#F9FAFB',
'border-left-color' => $colors['active_border_color'] ?? '#1e3a5f',
'font-weight' => '600',
]);
// Level indentation
$cssRules[] = $this->cssGenerator->generate('.toc-container .toc-level-3 .toc-link', [
'padding-left' => $spacing['level_three_padding_left'] ?? '1.5rem',
'font-size' => $typography['level_three_font_size'] ?? '0.85rem',
]);
$cssRules[] = $this->cssGenerator->generate('.toc-container .toc-level-4 .toc-link', [
'padding-left' => $spacing['level_four_padding_left'] ?? '2rem',
'font-size' => $typography['level_four_font_size'] ?? '0.8rem',
]);
// Scrollbar for toc-list
$cssRules[] = $this->cssGenerator->generate('.toc-container .toc-list::-webkit-scrollbar', [
'width' => $spacing['scrollbar_width'] ?? '6px',
]);
$cssRules[] = $this->cssGenerator->generate('.toc-container .toc-list::-webkit-scrollbar-track', [
'background' => $colors['scrollbar_track_color'] ?? '#F9FAFB',
'border-radius' => $effects['scrollbar_border_radius'] ?? '3px',
]);
$cssRules[] = $this->cssGenerator->generate('.toc-container .toc-list::-webkit-scrollbar-thumb', [
'background' => $colors['scrollbar_thumb_color'] ?? '#6B7280',
'border-radius' => $effects['scrollbar_border_radius'] ?? '3px',
]);
$cssRules[] = $this->cssGenerator->generate('.toc-container .toc-list::-webkit-scrollbar-thumb:hover', [
'background' => $colors['active_border_color'] ?? '#1e3a5f',
]);
// Responsive visibility
$showOnDesktop = $visibility['show_on_desktop'] ?? true;
$showOnMobile = $visibility['show_on_mobile'] ?? false;
if (!$showOnMobile) {
$cssRules[] = "@media (max-width: 991.98px) {
.toc-container { display: none !important; }
}";
}
if (!$showOnDesktop) {
$cssRules[] = "@media (min-width: 992px) {
.toc-container { display: none !important; }
}";
}
// Responsive layout adjustments
$cssRules[] = "@media (max-width: 991px) {
.sidebar-sticky {
position: relative !important;
top: 0 !important;
}
.toc-container {
margin-bottom: 2rem;
}
.toc-container .toc-list {
max-height: 300px;
}
}";
return implode("\n", $cssRules);
}
private function buildHTML(array $data, array $tocItems): string
{
$content = $data['content'] ?? [];
$title = $content['title'] ?? 'Tabla de Contenido';
// NOTA: El sticky behavior se maneja en el wrapper .sidebar-sticky de single.php
// El TOC no debe tener la clase sidebar-sticky - está dentro del wrapper
$html = '<div class="toc-container">';
$html .= sprintf(
'<h4 class="toc-title">%s</h4>',
esc_html($title)
);
$html .= '<ol class="list-unstyled toc-list">';
foreach ($tocItems as $item) {
$text = $item['text'] ?? '';
$anchor = $item['anchor'] ?? '';
$level = $item['level'] ?? 2;
if (empty($text) || empty($anchor)) {
continue;
}
$indentClass = $level > 2 ? 'toc-level-' . $level : '';
$html .= sprintf(
'<li class="%s"><a href="#%s" class="toc-link" data-level="%d">%s</a></li>',
esc_attr($indentClass),
esc_attr($anchor),
intval($level),
esc_html($text)
);
}
$html .= '</ol>';
$html .= '</div>';
return $html;
}
private function buildScript(array $data): string
{
$content = $data['content'] ?? [];
$behavior = $data['behavior'] ?? [];
$smoothScroll = $content['smooth_scroll'] ?? true;
$scrollOffset = intval($behavior['scroll_offset'] ?? 100);
if (!$smoothScroll) {
return '';
}
$script = <<<JS
<script>
document.addEventListener('DOMContentLoaded', function() {
var tocLinks = document.querySelectorAll('.toc-link');
var offsetTop = {$scrollOffset};
tocLinks.forEach(function(link) {
link.addEventListener('click', function(e) {
e.preventDefault();
var targetId = this.getAttribute('href');
var targetElement = document.querySelector(targetId);
if (targetElement) {
var elementPosition = targetElement.getBoundingClientRect().top;
var offsetPosition = elementPosition + window.pageYOffset - offsetTop;
window.scrollTo({
top: offsetPosition,
behavior: 'smooth'
});
}
});
});
// ScrollSpy
var sections = [];
tocLinks.forEach(function(link) {
var id = link.getAttribute('href').substring(1);
var section = document.getElementById(id);
if (section) {
sections.push({ id: id, element: section });
}
});
function updateActiveLink() {
var scrollPosition = window.pageYOffset + offsetTop + 50;
var currentSection = '';
sections.forEach(function(section) {
if (section.element.offsetTop <= scrollPosition) {
currentSection = section.id;
}
});
tocLinks.forEach(function(link) {
link.classList.remove('active');
if (link.getAttribute('href') === '#' + currentSection) {
link.classList.add('active');
}
});
}
window.addEventListener('scroll', updateActiveLink);
updateActiveLink();
});
</script>
JS;
return $script;
}
}

View File

@@ -0,0 +1,466 @@
<?php
declare(strict_types=1);
namespace ROITheme\Public\TopNotificationBar\Infrastructure\Ui;
use ROITheme\Shared\Domain\Contracts\RendererInterface;
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
use ROITheme\Shared\Domain\Entities\Component;
/**
* Class TopNotificationBarRenderer
*
* Renderizador del componente Top Notification Bar para el frontend.
*
* Responsabilidades:
* - Renderizar HTML del componente top-notification-bar
* - Delegar generación de CSS a CSSGeneratorInterface
* - Validar visibilidad (is_enabled, show_on_pages, hide_on_mobile)
* - Manejar visibilidad responsive con clases Bootstrap
* - Generar script para funcionalidad dismissible
* - Sanitizar todos los outputs
*
* NO responsable de:
* - Generar string CSS (delega a CSSGeneratorService)
* - Persistir datos (ya están en Component)
* - Lógica de negocio (está en Domain)
*
* Cumple con:
* - DIP: Recibe CSSGeneratorInterface por constructor
* - SRP: Una responsabilidad (renderizar este componente)
* - Clean Architecture: Infrastructure puede usar WordPress
*
* @package ROITheme\Public\topnotificationbar\infrastructure\ui
*/
final class TopNotificationBarRenderer implements RendererInterface
{
/**
* @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS
*/
public function __construct(
private CSSGeneratorInterface $cssGenerator
) {}
/**
* {@inheritDoc}
*/
public function render(Component $component): string
{
$data = $component->getData();
// Validar visibilidad general
if (!$this->isEnabled($data)) {
return '';
}
// Validar visibilidad por página
if (!$this->shouldShowOnCurrentPage($data)) {
return '';
}
// Generar CSS usando CSSGeneratorService
$css = $this->generateCSS($data);
// Generar HTML
$html = $this->buildHTML($data);
// Combinar todo
return sprintf(
"<style>%s</style>\n%s",
$css,
$html
);
}
/**
* {@inheritDoc}
*/
public function supports(string $componentType): bool
{
return $componentType === 'top-notification-bar';
}
/**
* Verificar si el componente está habilitado
*
* @param array $data Datos del componente
* @return bool
*/
private function isEnabled(array $data): bool
{
return ($data['visibility']['is_enabled'] ?? false) === true;
}
/**
* Verificar si debe mostrarse en la página actual
*
* @param array $data Datos del componente
* @return bool
*/
private function shouldShowOnCurrentPage(array $data): bool
{
$showOn = $data['visibility']['show_on_pages'] ?? 'all';
return match ($showOn) {
'all' => true,
'home' => is_front_page(),
'posts' => is_single(),
'pages' => is_page(),
'custom' => $this->isInCustomPages($data),
default => true,
};
}
/**
* Verificar si está en páginas personalizadas
*
* @param array $data Datos del componente
* @return bool
*/
private function isInCustomPages(array $data): bool
{
$pageIds = $data['visibility']['custom_page_ids'] ?? '';
if (empty($pageIds)) {
return false;
}
$allowedIds = array_map('trim', explode(',', $pageIds));
$currentId = (string) get_the_ID();
return in_array($currentId, $allowedIds, true);
}
/**
* Verificar si el componente fue dismissed por el usuario
*
* @param array $data Datos del componente
* @return bool
*/
private function isDismissed(array $data): bool
{
if (!$this->isDismissible($data)) {
return false;
}
$cookieName = 'roi_notification_bar_dismissed';
return isset($_COOKIE[$cookieName]) && $_COOKIE[$cookieName] === '1';
}
/**
* Verificar si el componente es dismissible
*
* @param array $data Datos del componente
* @return bool
*/
private function isDismissible(array $data): bool
{
return ($data['behavior']['is_dismissible'] ?? false) === true;
}
/**
* Generar CSS usando CSSGeneratorService
*
* @param array $data Datos del componente
* @return string CSS generado
*/
private function generateCSS(array $data): string
{
$css = '';
// Estilos base de la barra
$baseStyles = [
'background_color' => $data['styles']['background_color'] ?? '#0E2337',
'color' => $data['styles']['text_color'] ?? '#FFFFFF',
'font_size' => $data['styles']['font_size'] ?? '0.9rem',
'padding' => $data['styles']['padding'] ?? '0.5rem 0',
'width' => '100%',
'z_index' => '1050',
];
$css .= $this->cssGenerator->generate('.top-notification-bar', $baseStyles);
// Estilos del ícono
$iconStyles = [
'color' => $data['styles']['icon_color'] ?? '#FF8600',
];
$css .= "\n" . $this->cssGenerator->generate('.top-notification-bar .notification-icon', $iconStyles);
// Estilos de la etiqueta (label)
$labelStyles = [
'color' => $data['styles']['label_color'] ?? '#FF8600',
];
$css .= "\n" . $this->cssGenerator->generate('.top-notification-bar .notification-label', $labelStyles);
// Estilos del enlace
$linkStyles = [
'color' => $data['styles']['link_color'] ?? '#FFFFFF',
];
$css .= "\n" . $this->cssGenerator->generate('.top-notification-bar .notification-link', $linkStyles);
// Estilos del enlace hover
$linkHoverStyles = [
'color' => $data['styles']['link_hover_color'] ?? '#FF8600',
];
$css .= "\n" . $this->cssGenerator->generate('.top-notification-bar .notification-link:hover', $linkHoverStyles);
// Estilos del ícono personalizado
$customIconStyles = [
'width' => '24px',
'height' => '24px',
];
$css .= "\n" . $this->cssGenerator->generate('.top-notification-bar .custom-icon', $customIconStyles);
return $css;
}
/**
* Generar HTML del componente
*
* @param array $data Datos del componente
* @return string HTML generado
*/
private function buildHTML(array $data): string
{
$classes = $this->buildClasses($data);
$content = $this->buildContent($data);
return sprintf(
'<div class="%s">%s</div>',
esc_attr($classes),
$content
);
}
/**
* Construir clases CSS del componente
*
* @param array $data Datos del componente
* @return string Clases CSS
*/
private function buildClasses(array $data): string
{
return 'top-notification-bar';
}
/**
* Construir atributos data para dismissible
*
* @param array $data Datos del componente
* @return string Atributos HTML
*/
private function buildDismissAttributes(array $data): string
{
if (!$this->isDismissible($data)) {
return '';
}
$days = (int) ($data['behavior']['dismissible_cookie_days'] ?? 7);
return sprintf(' data-dismissible-days="%d"', $days);
}
/**
* Construir contenido del componente
*
* @param array $data Datos del componente
* @return string HTML del contenido
*/
private function buildContent(array $data): string
{
$html = '<div class="container">';
$html .= '<div class="d-flex align-items-center justify-content-center">';
// Ícono
$html .= $this->buildIcon($data);
// Texto del anuncio
$html .= $this->buildAnnouncementText($data);
// Enlace
$html .= $this->buildLink($data);
$html .= '</div>';
$html .= '</div>';
return $html;
}
/**
* Construir ícono del componente
*
* @param array $data Datos del componente
* @return string HTML del ícono
*/
private function buildIcon(array $data): string
{
// Siempre usar Bootstrap icon desde content.icon_class
$iconClass = $data['content']['icon_class'] ?? 'bi-megaphone-fill';
// Asegurar prefijo 'bi-'
if (strpos($iconClass, 'bi-') !== 0) {
$iconClass = 'bi-' . $iconClass;
}
return sprintf(
'<i class="bi %s notification-icon me-2"></i>',
esc_attr($iconClass)
);
}
/**
* Construir texto del anuncio
*
* @param array $data Datos del componente
* @return string HTML del texto
*/
private function buildAnnouncementText(array $data): string
{
$label = $data['content']['label_text'] ?? '';
$text = $data['content']['message_text'] ?? '';
if (empty($text)) {
return '';
}
$html = '<span>';
if (!empty($label)) {
$html .= sprintf('<strong class="notification-label">%s</strong> ', esc_html($label));
}
$html .= esc_html($text);
$html .= '</span>';
return $html;
}
/**
* Construir enlace de acción
*
* @param array $data Datos del componente
* @return string HTML del enlace
*/
private function buildLink(array $data): string
{
$linkText = $data['content']['link_text'] ?? '';
$linkUrl = $data['content']['link_url'] ?? '#';
if (empty($linkText)) {
return '';
}
return sprintf(
'<a href="%s" class="notification-link ms-2 text-decoration-underline">%s</a>',
esc_url($linkUrl),
esc_html($linkText)
);
}
/**
* Construir botón de cerrar
*
* @return string HTML del botón
*/
private function buildDismissButton(): string
{
return '<button type="button" class="btn-close btn-close-white ms-3 roi-dismiss-notification" aria-label="Cerrar"></button>';
}
/**
* Construir estilos CSS de animaciones
*
* @param array $data Datos del componente
* @return string CSS de animaciones
*/
private function buildAnimationStyles(array $data): string
{
$animationType = $data['visual_effects']['animation_type'] ?? 'slide-down';
$animations = [
'slide-down' => [
'keyframes' => '@keyframes roiSlideDown { from { transform: translateY(-100%); opacity: 0; } to { transform: translateY(0); opacity: 1; } }',
'animation' => 'roiSlideDown 0.5s ease-out',
],
'fade-in' => [
'keyframes' => '@keyframes roiFadeIn { from { opacity: 0; } to { opacity: 1; } }',
'animation' => 'roiFadeIn 0.5s ease-out',
],
];
$anim = $animations[$animationType] ?? $animations['slide-down'];
return sprintf(
"%s\n.top-notification-bar.roi-animated.roi-%s { animation: %s; }",
$anim['keyframes'],
$animationType,
$anim['animation']
);
}
/**
* Construir script para funcionalidad dismissible
*
* @param array $data Datos del componente
* @return string JavaScript
*/
private function buildDismissScript(array $data): string
{
$days = (int) ($data['behavior']['dismissible_cookie_days'] ?? 7);
return sprintf(
'<script>
document.addEventListener("DOMContentLoaded", function() {
const dismissBtn = document.querySelector(".roi-dismiss-notification");
if (dismissBtn) {
dismissBtn.addEventListener("click", function() {
const bar = document.querySelector(".top-notification-bar");
if (bar) {
bar.style.display = "none";
}
const days = %d;
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
const expires = "expires=" + date.toUTCString();
document.cookie = "roi_notification_bar_dismissed=1;" + expires + ";path=/";
});
}
});
</script>',
$days
);
}
/**
* Obtiene las clases CSS de Bootstrap para visibilidad responsive
*
* Implementa tabla de decisión según especificación (10.03):
* - Desktop Y Mobile = null (visible en ambos)
* - Solo Desktop = 'd-none d-lg-block'
* - Solo Mobile = 'd-lg-none'
* - Ninguno = 'd-none' (oculto)
*
* @param bool $desktop Mostrar en desktop
* @param bool $mobile Mostrar en mobile
* @return string|null Clases CSS o null si visible en ambos
*/
private function getVisibilityClasses(bool $desktop, bool $mobile): ?string
{
// Desktop Y Mobile = visible en ambos dispositivos
if ($desktop && $mobile) {
return null; // Sin clases = visible siempre
}
// Solo Desktop
if ($desktop && !$mobile) {
return 'd-none d-lg-block';
}
// Solo Mobile
if (!$desktop && $mobile) {
return 'd-lg-none';
}
// Ninguno = oculto completamente
return 'd-none';
}
}

View File

@@ -1,257 +0,0 @@
# Contexto Admin - Administración de Componentes
## Propósito
El contexto `admin/` contiene **todo el código relacionado con la administración de componentes** en el panel
de WordPress. Cada componente tiene su propia carpeta con su Clean Architecture completa.
## Filosofía: Context-First Architecture
En lugar de separar por capas (Domain, Application, Infrastructure), separamos por **contextos**
(admin, public, shared) y cada contexto tiene sus propias capas internas.
## Estructura (Fase-00)
En Fase-00 solo creamos la estructura base. Los componentes se crearán en fases posteriores:
```
admin/
├── README.md (este archivo)
└── .gitkeep (preserva directorio en Git)
```
## Estructura Futura (Post Fase-00)
```
admin/
├── Navbar/ # Componente Navbar
│ ├── Domain/
│ │ ├── NavbarComponent.php # Entidad
│ │ └── NavbarRepositoryInterface.php
│ ├── Application/
│ │ ├── SaveNavbarUseCase.php # Caso de uso
│ │ └── DTO/
│ │ └── NavbarSettingsDTO.php
│ ├── Infrastructure/
│ │ ├── Persistence/
│ │ │ └── WordPressNavbarRepository.php
│ │ ├── UI/
│ │ │ ├── NavbarAdminPage.php # Página de admin
│ │ │ ├── NavbarForm.php # Formulario
│ │ │ └── views/
│ │ │ └── navbar-settings.php
│ │ └── API/
│ │ └── NavbarAjaxHandler.php # AJAX endpoints
│ └── README.md
├── Footer/ # Otro componente
│ └── (misma estructura)
└── (más componentes...)
```
## Principios del Contexto Admin
1. **Aislamiento**: Cada componente es independiente
2. **Clean Architecture**: Cada componente tiene Domain, Application, Infrastructure
3. **Sin acoplamiento**: Un componente no depende de otro directamente
4. **Código compartido**: Va en `shared/` si es usado por múltiples componentes
## Responsabilidades
El contexto `admin/` se encarga de:
**Formularios de configuración** de componentes
**Validación de entrada** del usuario administrador
**Guardado de configuraciones** en la base de datos
**Páginas de administración** en el panel de WordPress
**AJAX endpoints** para operaciones administrativas
**Permisos y capabilities** de administrador
**NO** se encarga de:
- Renderizado frontend de componentes (va en `public/`)
- Lógica compartida entre contextos (va en `shared/`)
- Configuraciones globales del tema
## Ejemplo de Flujo de Admin
### 1. Usuario administra el Navbar
```
Usuario Admin (navegador)
NavbarAdminPage.php (muestra formulario)
Usuario envía formulario via AJAX
NavbarAjaxHandler.php (recibe request)
SaveNavbarUseCase.php (orquesta la lógica)
WordPressNavbarRepository.php (guarda en DB)
Respuesta JSON al navegador
```
### 2. Código de ejemplo
```php
// admin/Navbar/Application/SaveNavbarUseCase.php
namespace ROITheme\Admin\Navbar\Application;
use ROITheme\Admin\Navbar\Domain\NavbarRepositoryInterface;
use ROITheme\Shared\Application\Contracts\ValidationServiceInterface;
final class SaveNavbarUseCase
{
public function __construct(
private readonly NavbarRepositoryInterface $repository,
private readonly ValidationServiceInterface $validator
) {}
public function execute(array $data): void
{
$validated = $this->validator->validate($data, [
'logo_url' => 'url',
'menu_items' => 'array',
'sticky' => 'bool',
]);
if ($this->validator->fails()) {
throw new ValidationException($this->validator->errors());
}
$this->repository->save($validated);
}
}
```
```php
// admin/Navbar/Infrastructure/UI/NavbarAdminPage.php
namespace ROITheme\Admin\Navbar\Infrastructure\UI;
final class NavbarAdminPage
{
public function register(): void
{
add_action('admin_menu', [$this, 'addMenuPage']);
}
public function addMenuPage(): void
{
add_menu_page(
'Navbar Settings',
'Navbar',
'manage_options',
'roi-navbar-settings',
[$this, 'render'],
'dashicons-menu',
30
);
}
public function render(): void
{
require __DIR__ . '/views/navbar-settings.php';
}
}
```
## Relación con Otros Contextos
```
admin/ → Administra configuraciones
↓ guarda
Base de Datos → Almacena settings
↓ lee
public/ → Renderiza componentes en frontend
```
**Clave**: `admin/` y `public/` NO se hablan directamente. Se comunican a través de la base de datos.
## Reglas de Dependencia
**PUEDE** depender de:
- `shared/Domain/` (Value Objects, Exceptions)
- `shared/Application/` (Contracts, Services)
- `shared/Infrastructure/` (Implementaciones de servicios)
- WordPress admin functions (`add_menu_page`, `add_settings_section`, etc.)
**NO PUEDE** depender de:
- `public/` (contexto independiente)
- Código de frontend (JS/CSS va en Infrastructure/UI/)
## Testing
### Tests Unitarios
```php
// tests/Unit/Admin/Navbar/Application/SaveNavbarUseCaseTest.php
public function test_saves_valid_navbar_settings()
{
$repository = $this->createMock(NavbarRepositoryInterface::class);
$validator = $this->createMock(ValidationServiceInterface::class);
$useCase = new SaveNavbarUseCase($repository, $validator);
$useCase->execute(['logo_url' => 'http://example.com/logo.png']);
// Assertions...
}
```
### Tests de Integración
```php
// tests/Integration/Admin/Navbar/Infrastructure/WordPressNavbarRepositoryTest.php
public function test_saves_and_retrieves_navbar_settings()
{
$repository = new WordPressNavbarRepository();
$settings = ['logo_url' => 'http://example.com/logo.png'];
$repository->save($settings);
$retrieved = $repository->get();
$this->assertEquals($settings, $retrieved);
}
```
### Tests E2E (Playwright)
```php
// tests/E2E/Admin/NavbarAdminPageTest.php
public function test_admin_can_save_navbar_settings()
{
$this->loginAsAdmin();
$this->visit('/wp-admin/admin.php?page=roi-navbar-settings');
$this->fillField('logo_url', 'http://example.com/logo.png');
$this->click('Save Settings');
$this->see('Settings saved successfully');
}
```
## Cuándo Agregar Código Aquí
Agrega código a `admin/` cuando:
- Creas un nuevo componente administrable
- Necesitas una página de configuración en el panel de WordPress
- Manejas formularios de administrador
- Procesas AJAX desde el admin
- Validas o guardas configuraciones
No agregues aquí:
- Renderizado frontend (va en `public/`)
- Lógica compartida (va en `shared/`)
- Configuraciones globales del tema
## Estado Actual (Fase-00)
En Fase-00, `admin/` solo tiene la estructura base. Los componentes se crearán en las siguientes fases:
- **Fase-1**: Estructura base e infraestructura
- **Fase-2**: Migración de base de datos
- **Fase-3**: Implementación de componentes admin
- **Fase-4+**: Componentes adicionales
## Próximos Pasos
1. Crear primer componente en Fase-3 (ej: Navbar)
2. Implementar Domain layer del componente
3. Implementar Application layer (casos de uso)
4. Implementar Infrastructure layer (WordPress integration)
5. Crear tests unitarios e integración
6. Repetir para cada componente

View File

@@ -1,511 +0,0 @@
/**
* Admin Panel Styles
*
* Estilos base para el panel de administración
*
* @package ROI_Theme
* @since 2.0.0
*/
/* ========================================
Container
======================================== */
.roi-admin-panel {
max-width: 1400px;
margin: 20px auto;
}
/* ========================================
Header
======================================== */
.roi-admin-panel h1 {
margin-bottom: 10px;
}
.roi-admin-panel .description {
color: #666;
margin-bottom: 20px;
}
/* ========================================
Tabs
======================================== */
.nav-tabs {
border-bottom: 2px solid #dee2e6;
}
.nav-tabs .nav-link {
color: #666;
border: none;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
}
.nav-tabs .nav-link:hover {
color: #0073aa;
border-bottom-color: #0073aa;
}
.nav-tabs .nav-link.active {
color: #0073aa;
font-weight: 600;
border-bottom-color: #0073aa;
background-color: transparent;
}
/* ========================================
Tab Content
======================================== */
.tab-content {
background: #fff;
padding: 20px;
border: 1px solid #ddd;
border-radius: 4px;
}
.tab-pane h3 {
margin-top: 0;
margin-bottom: 15px;
font-size: 18px;
}
.tab-pane h4 {
margin-top: 25px;
margin-bottom: 10px;
font-size: 16px;
color: #333;
}
/* ========================================
Form Sections
======================================== */
.form-section {
padding-bottom: 20px;
border-bottom: 1px solid #eee;
}
.form-section:last-child {
border-bottom: none;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
font-weight: 600;
margin-bottom: 5px;
color: #333;
}
.form-group input[type="text"],
.form-group input[type="url"],
.form-group input[type="email"],
.form-group input[type="number"],
.form-group select,
.form-group textarea {
max-width: 600px;
}
.form-group .form-text {
margin-top: 5px;
font-size: 13px;
}
.form-group .form-text code {
background: #f5f5f5;
padding: 2px 5px;
border-radius: 3px;
font-size: 12px;
}
/* ========================================
Action Buttons
======================================== */
.admin-actions {
padding: 20px;
background: #f9f9f9;
border-top: 1px solid #ddd;
border-radius: 4px;
}
.admin-actions .button-primary {
font-size: 14px;
padding: 8px 20px;
height: auto;
}
.admin-actions .button-primary i {
vertical-align: middle;
}
/* ========================================
Responsive
======================================== */
@media (max-width: 782px) {
.roi-admin-panel {
margin: 10px;
}
.tab-content {
padding: 15px;
}
.form-group input[type="text"],
.form-group input[type="url"],
.form-group input[type="email"],
.form-group select,
.form-group textarea {
max-width: 100%;
}
}
/* ============================================
MEJORAS ESPECÍFICAS PARA ADMIN PANEL
============================================ */
/* Variables CSS */
:root {
--color-navy-primary: #1E3A5F;
--color-navy-light: #2C5282;
--color-navy-dark: #0E2337;
--color-orange-primary: #FF8600;
--color-orange-hover: #FF6B35;
--color-neutral-50: #F9FAFB;
--color-neutral-100: #F3F4F6;
--color-neutral-600: #6B7280;
--color-neutral-700: #4B5563;
}
/* Colores de marca como clases de utilidad */
.text-navy-primary { color: var(--color-navy-primary) !important; }
.text-navy-dark { color: var(--color-navy-dark) !important; }
.text-orange-primary { color: var(--color-orange-primary) !important; }
.text-neutral-600 { color: var(--color-neutral-600) !important; }
.text-neutral-700 { color: var(--color-neutral-700) !important; }
.bg-neutral-50 { background-color: var(--color-neutral-50) !important; }
.bg-neutral-100 { background-color: var(--color-neutral-100) !important; }
/* Tab Header mejorado */
.tab-header {
padding: 1.5rem;
background: linear-gradient(135deg, rgba(30, 58, 95, 0.03) 0%, rgba(255, 134, 0, 0.02) 100%);
border-radius: 8px;
margin-bottom: 2rem;
border-left: 4px solid var(--color-orange-primary);
}
/* Section Title con icono */
.section-title {
color: var(--color-navy-primary);
font-size: 1.25rem;
font-weight: 700;
margin-bottom: 1.5rem;
padding-bottom: 0.75rem;
border-bottom: 2px solid var(--color-neutral-100);
display: flex;
align-items: center;
gap: 0.75rem;
}
.section-title .title-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
background: linear-gradient(135deg, var(--color-orange-primary), var(--color-orange-hover));
border-radius: 8px;
color: white;
font-size: 1rem;
}
/* Form Section Cards mejorados */
.form-section.card {
border: 1px solid var(--color-neutral-100);
transition: all 0.3s ease;
}
.form-section.card:hover {
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.08);
border-color: var(--color-neutral-100);
}
/* Toggle Container mejorado */
.toggle-container {
background: var(--color-neutral-50);
padding: 1rem;
border-radius: 8px;
border: 1px solid var(--color-neutral-100);
}
/* Switch más grande */
.form-switch-lg .form-check-input {
width: 3rem;
height: 1.5rem;
cursor: pointer;
}
.form-switch-lg .form-check-input:checked {
background-color: var(--color-orange-primary);
border-color: var(--color-orange-primary);
}
.form-switch-lg .form-check-input:focus {
box-shadow: 0 0 0 0.25rem rgba(255, 134, 0, 0.25);
border-color: var(--color-orange-primary);
}
.form-switch-lg .form-check-label {
margin-left: 0.5rem;
font-weight: 500;
}
/* Input Group merge (sin borde en el medio) */
.input-group-merge .input-group-text {
border-right: 0;
background-color: var(--color-neutral-50);
}
.input-group-merge .form-control {
border-left: 0;
}
.input-group-merge .form-control:focus {
border-color: var(--color-orange-primary);
box-shadow: none;
}
/* Color Picker mejorado */
.color-picker-wrapper .form-control-color {
width: 100%;
height: 60px;
border-radius: 8px;
border: 2px solid var(--color-neutral-100);
cursor: pointer;
transition: all 0.3s ease;
}
.color-picker-wrapper .form-control-color:hover {
border-color: var(--color-orange-primary);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(255, 134, 0, 0.15);
}
.color-picker-wrapper .form-control-color::-webkit-color-swatch {
border-radius: 4px;
border: none;
}
.color-picker-wrapper .color-preview-text {
text-align: center;
}
.color-picker-wrapper .color-preview-text code {
display: block;
font-size: 0.875rem;
font-weight: 600;
margin-bottom: 0.25rem;
}
/* Alert personalizado */
.alert-info-custom {
background: linear-gradient(135deg, rgba(255, 134, 0, 0.08), rgba(255, 134, 0, 0.03));
border: 1px solid rgba(255, 134, 0, 0.2);
border-radius: 8px;
padding: 1rem;
}
.alert-info-custom .alert-heading {
color: var(--color-navy-primary);
font-weight: 700;
}
.alert-info-custom p {
color: var(--color-neutral-600);
}
/* Preview Container */
.preview-container {
position: relative;
min-height: 150px;
}
.top-bar-preview {
animation: fadeInUp 0.5s ease;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Botones de marca */
.btn-navy-primary {
background-color: var(--color-navy-primary);
border-color: var(--color-navy-primary);
color: white;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-navy-primary:hover {
background-color: var(--color-navy-light);
border-color: var(--color-navy-light);
color: white;
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(30, 58, 95, 0.2);
}
.btn-orange-primary {
background-color: var(--color-orange-primary);
border-color: var(--color-orange-primary);
color: white;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-orange-primary:hover {
background-color: var(--color-orange-hover);
border-color: var(--color-orange-hover);
color: white;
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(255, 134, 0, 0.3);
}
/* Sticky Footer Actions */
.sticky-bottom {
position: sticky;
bottom: 0;
z-index: 10;
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.05);
}
/* Progress bar para textarea */
.progress {
background-color: var(--color-neutral-100);
height: 4px;
border-radius: 2px;
overflow: hidden;
}
.progress .progress-bar {
transition: width 0.3s ease;
}
.progress .progress-bar.bg-orange-primary {
background-color: var(--color-orange-primary);
}
/* Badges personalizados */
.badge.bg-neutral-100 {
background-color: var(--color-neutral-100) !important;
}
.badge.text-neutral-600 {
color: var(--color-neutral-600) !important;
}
/* Form control improvements */
.form-control-lg {
font-size: 1rem;
padding: 0.75rem 1rem;
}
.form-select-lg {
font-size: 1rem;
padding: 0.75rem 1rem;
}
/* Gap utilities */
.g-4 {
gap: 1.5rem;
}
/* Border utilities */
.border-0 {
border: 0 !important;
}
.border-neutral-100 {
border-color: var(--color-neutral-100) !important;
}
/* Shadow utilities */
.shadow-sm {
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06) !important;
}
/* Rounded utilities */
.rounded-3 {
border-radius: 0.5rem !important;
}
.rounded-bottom {
border-bottom-left-radius: 0.5rem !important;
border-bottom-right-radius: 0.5rem !important;
}
/* Card utilities */
.card {
background-color: #fff;
border: 1px solid rgba(0, 0, 0, 0.125);
border-radius: 0.5rem;
}
.card-body {
padding: 1.5rem;
}
/* Typography utilities */
.fw-bold {
font-weight: 700 !important;
}
.fw-medium {
font-weight: 500 !important;
}
.small {
font-size: 0.875rem;
}
/* Responsive mejoras */
@media (max-width: 768px) {
.tab-header {
padding: 1rem;
}
.tab-header .d-flex {
flex-direction: column;
gap: 1rem;
}
.section-title {
font-size: 1.1rem;
}
.color-picker-wrapper .form-control-color {
height: 50px;
}
.form-switch-lg .form-check-input {
width: 2.5rem;
height: 1.25rem;
}
}

View File

@@ -1,471 +0,0 @@
/**
* Theme Options Admin Styles
*
* @package ROI_Theme
* @since 1.0.0
*/
/* Main Container */
.roi-theme-options {
margin: 20px 20px 0 0;
}
/* Header */
.roi-options-header {
background: #fff;
border: 1px solid #c3c4c7;
padding: 20px;
margin: 20px 0;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 1px 1px rgba(0,0,0,.04);
}
.roi-options-logo h2 {
margin: 0;
font-size: 24px;
color: #1d2327;
display: inline-block;
}
.roi-options-logo .version {
background: #2271b1;
color: #fff;
padding: 3px 8px;
border-radius: 3px;
font-size: 12px;
margin-left: 10px;
}
.roi-options-actions {
display: flex;
gap: 10px;
}
.roi-options-actions .button .dashicons {
margin-top: 3px;
margin-right: 3px;
}
/* Form */
.roi-options-form {
background: #fff;
border: 1px solid #c3c4c7;
box-shadow: 0 1px 1px rgba(0,0,0,.04);
}
/* Tabs Container */
.roi-options-container {
display: flex;
min-height: 600px;
}
/* Tabs Navigation */
.roi-tabs-nav {
width: 200px;
background: #f6f7f7;
border-right: 1px solid #c3c4c7;
}
.roi-tabs-nav ul {
margin: 0;
padding: 0;
list-style: none;
}
.roi-tabs-nav li {
margin: 0;
padding: 0;
border-bottom: 1px solid #c3c4c7;
}
.roi-tabs-nav li:first-child {
border-top: 1px solid #c3c4c7;
}
.roi-tabs-nav a {
display: block;
padding: 15px 20px;
color: #50575e;
text-decoration: none;
transition: all 0.2s;
position: relative;
}
.roi-tabs-nav a .dashicons {
margin-right: 8px;
color: #787c82;
}
.roi-tabs-nav a:hover {
background: #fff;
color: #2271b1;
}
.roi-tabs-nav a:hover .dashicons {
color: #2271b1;
}
.roi-tabs-nav li.active a {
background: #fff;
color: #2271b1;
font-weight: 600;
border-left: 3px solid #2271b1;
padding-left: 17px;
}
.roi-tabs-nav li.active a .dashicons {
color: #2271b1;
}
/* Tabs Content */
.roi-tabs-content {
flex: 1;
padding: 30px;
}
.roi-tab-pane {
display: none;
}
.roi-tab-pane.active {
display: block;
}
.roi-tab-pane h2 {
margin: 0 0 10px 0;
font-size: 23px;
font-weight: 400;
line-height: 1.3;
}
.roi-tab-pane > p.description {
margin: 0 0 20px 0;
color: #646970;
}
.roi-tab-pane h3 {
margin: 30px 0 0 0;
padding: 15px 0 10px 0;
border-top: 1px solid #dcdcde;
font-size: 18px;
}
/* Form Table */
.roi-tab-pane .form-table {
margin-top: 20px;
}
.roi-tab-pane .form-table th {
padding: 20px 10px 20px 0;
width: 200px;
}
.roi-tab-pane .form-table td {
padding: 15px 10px;
}
/* Toggle Switch */
.roi-switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.roi-switch input {
opacity: 0;
width: 0;
height: 0;
}
.roi-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 24px;
}
.roi-slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .roi-slider {
background-color: #2271b1;
}
input:focus + .roi-slider {
box-shadow: 0 0 1px #2271b1;
}
input:checked + .roi-slider:before {
transform: translateX(26px);
}
/* Image Upload */
.roi-image-upload {
max-width: 600px;
}
.roi-image-preview {
margin-bottom: 10px;
border: 1px solid #c3c4c7;
background: #f6f7f7;
padding: 10px;
min-height: 100px;
display: flex;
align-items: center;
justify-content: center;
}
.roi-image-preview:empty {
display: none;
}
.roi-preview-image {
max-width: 100%;
height: auto;
display: block;
}
.roi-upload-image,
.roi-remove-image {
margin-right: 10px;
}
/* Submit Button */
.roi-options-form .submit {
margin: 0;
padding: 20px 30px;
border-top: 1px solid #c3c4c7;
background: #f6f7f7;
}
/* Modal */
.roi-modal {
display: none;
position: fixed;
z-index: 100000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.5);
}
.roi-modal-content {
background-color: #fff;
margin: 10% auto;
padding: 30px;
border: 1px solid #c3c4c7;
width: 80%;
max-width: 600px;
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
border-radius: 4px;
}
.roi-modal-close {
color: #646970;
float: right;
font-size: 28px;
font-weight: bold;
line-height: 20px;
cursor: pointer;
}
.roi-modal-close:hover,
.roi-modal-close:focus {
color: #1d2327;
}
.roi-modal-content h2 {
margin-top: 0;
}
.roi-modal-content textarea {
font-family: 'Courier New', Courier, monospace;
font-size: 12px;
}
/* Notices */
.roi-notice {
padding: 12px;
margin: 20px 0;
border-left: 4px solid;
background: #fff;
box-shadow: 0 1px 1px rgba(0,0,0,.04);
}
.roi-notice.success {
border-left-color: #00a32a;
}
.roi-notice.error {
border-left-color: #d63638;
}
.roi-notice.warning {
border-left-color: #dba617;
}
.roi-notice.info {
border-left-color: #2271b1;
}
/* Code Editor */
textarea.code {
font-family: 'Courier New', Courier, monospace;
font-size: 13px;
line-height: 1.5;
}
/* Responsive */
@media screen and (max-width: 782px) {
.roi-options-container {
flex-direction: column;
}
.roi-tabs-nav {
width: 100%;
border-right: none;
border-bottom: 1px solid #c3c4c7;
}
.roi-tabs-nav ul {
display: flex;
flex-wrap: wrap;
}
.roi-tabs-nav li {
flex: 1;
min-width: 50%;
border-right: 1px solid #c3c4c7;
border-bottom: none;
}
.roi-tabs-nav li:first-child {
border-top: none;
}
.roi-tabs-nav a {
text-align: center;
padding: 12px 10px;
font-size: 13px;
}
.roi-tabs-nav a .dashicons {
display: block;
margin: 0 auto 5px;
}
.roi-tabs-nav li.active a {
border-left: none;
border-bottom: 3px solid #2271b1;
padding-left: 10px;
}
.roi-tabs-content {
padding: 20px;
}
.roi-options-header {
flex-direction: column;
gap: 15px;
}
.roi-options-actions {
width: 100%;
flex-direction: column;
}
.roi-options-actions .button {
width: 100%;
text-align: center;
}
.roi-tab-pane .form-table th {
width: auto;
padding: 15px 10px 5px 0;
display: block;
}
.roi-tab-pane .form-table td {
display: block;
padding: 5px 10px 15px 0;
}
}
/* Loading Spinner */
.roi-spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(0,0,0,.1);
border-radius: 50%;
border-top-color: #2271b1;
animation: roispin 1s ease-in-out infinite;
}
@keyframes roispin {
to { transform: rotate(360deg); }
}
/* Helper Classes */
.roi-hidden {
display: none !important;
}
.roi-text-center {
text-align: center;
}
.roi-mt-20 {
margin-top: 20px;
}
.roi-mb-20 {
margin-bottom: 20px;
}
/* Color Picker */
.wp-picker-container {
display: inline-block;
}
/* Field Dependencies */
.roi-field-dependency {
opacity: 0.5;
pointer-events: none;
}
/* Success Animation */
@keyframes roisaved {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
.roi-saved {
animation: roisaved 0.3s ease-in-out;
}

View File

@@ -1,219 +0,0 @@
/**
* Admin Panel Application
*
* Gestión de configuraciones de componentes del tema
*
* @package ROI_Theme
* @since 2.0.0
*/
const AdminPanel = {
/**
* Estado de la aplicación
*/
STATE: {
settings: {},
hasChanges: false,
isLoading: false
},
/**
* Inicializar aplicación
*/
init() {
this.bindEvents();
this.loadSettings();
},
/**
* Vincular eventos
*/
bindEvents() {
// Botón guardar
const saveBtn = document.getElementById('saveSettings');
if (saveBtn) {
saveBtn.addEventListener('click', () => {
this.saveSettings();
});
}
// Detectar cambios en formularios
const enableSaveButton = () => {
this.STATE.hasChanges = true;
const btn = document.getElementById('saveSettings');
if (btn) btn.disabled = false;
};
document.querySelectorAll('input, select, textarea').forEach(input => {
// Evento 'input' se dispara mientras se escribe (tiempo real)
input.addEventListener('input', enableSaveButton);
// Evento 'change' se dispara cuando pierde foco (para select y checkboxes)
input.addEventListener('change', enableSaveButton);
});
// Tabs
const tabs = document.querySelectorAll('.nav-tabs .nav-link');
tabs.forEach(tab => {
tab.addEventListener('click', (e) => {
e.preventDefault();
this.switchTab(tab);
});
});
},
/**
* Cambiar tab
*/
switchTab(tab) {
// Remover active de todos
document.querySelectorAll('.nav-tabs .nav-link').forEach(t => {
t.classList.remove('active');
});
document.querySelectorAll('.tab-pane').forEach(pane => {
pane.classList.remove('show', 'active');
});
// Activar seleccionado
tab.classList.add('active');
const targetId = tab.getAttribute('data-bs-target').substring(1);
const targetPane = document.getElementById(targetId);
if (targetPane) {
targetPane.classList.add('show', 'active');
}
},
/**
* Cargar configuraciones desde servidor
*/
async loadSettings() {
this.STATE.isLoading = true;
this.showSpinner(true);
try {
const response = await axios({
method: 'POST',
url: roiAdminData.ajaxUrl,
data: new URLSearchParams({
action: 'roi_get_settings',
nonce: roiAdminData.nonce
})
});
if (response.data.success) {
this.STATE.settings = response.data.data;
this.renderAllComponents();
} else {
this.showNotice('Error al cargar configuraciones', 'error');
}
} catch (error) {
console.error('Error loading settings:', error);
this.showNotice('Error de conexión', 'error');
} finally {
this.STATE.isLoading = false;
this.showSpinner(false);
}
},
/**
* Guardar configuraciones al servidor
*/
async saveSettings() {
if (!this.STATE.hasChanges) {
this.showNotice('No hay cambios para guardar', 'info');
return;
}
this.showSpinner(true);
try {
const formData = this.collectFormData();
// Crear FormData para WordPress AJAX
const postData = new URLSearchParams();
postData.append('action', 'roi_save_settings');
postData.append('nonce', roiAdminData.nonce);
// Agregar components como JSON string
postData.append('components', JSON.stringify(formData.components));
const response = await axios({
method: 'POST',
url: roiAdminData.ajaxUrl,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: postData
});
if (response.data.success) {
this.STATE.hasChanges = false;
const btn = document.getElementById('saveSettings');
if (btn) btn.disabled = true;
this.showNotice('Configuración guardada correctamente', 'success');
} else {
this.showNotice(response.data.data.message || 'Error al guardar', 'error');
}
} catch (error) {
console.error('Error saving settings:', error);
this.showNotice('Error de conexión', 'error');
} finally {
this.showSpinner(false);
}
},
/**
* Recolectar datos del formulario
* Cada componente agregará su sección aquí cuando se implemente
*/
collectFormData() {
return {
components: {
// Los componentes se agregarán aquí cuando se ejecute el algoritmo
}
};
},
/**
* Renderizar todos los componentes
*/
renderAllComponents() {
const components = this.STATE.settings.components || {};
// Los métodos render de componentes se llamarán aquí cuando se implementen
},
/**
* Utilidad: Mostrar spinner
*/
showSpinner(show) {
const spinner = document.querySelector('.spinner');
if (spinner) {
spinner.style.display = show ? 'inline-block' : 'none';
}
},
/**
* Utilidad: Mostrar notificación
*/
showNotice(message, type = 'info') {
// WordPress admin notices
const noticeDiv = document.createElement('div');
noticeDiv.className = `notice notice-${type} is-dismissible`;
noticeDiv.innerHTML = `<p>${message}</p>`;
const container = document.querySelector('.roi-admin-panel');
if (container) {
container.insertBefore(noticeDiv, container.firstChild);
// Auto-dismiss después de 5 segundos
setTimeout(() => {
noticeDiv.remove();
}, 5000);
}
}
};
// Inicializar cuando el DOM esté listo
document.addEventListener('DOMContentLoaded', () => {
AdminPanel.init();
});

View File

@@ -1,440 +0,0 @@
/**
* Theme Options Admin JavaScript
*
* @package ROI_Theme
* @since 1.0.0
*/
(function($) {
'use strict';
var ROIThemeOptions = {
/**
* Initialize
*/
init: function() {
this.tabs();
this.imageUpload();
this.resetOptions();
this.exportOptions();
this.importOptions();
this.formValidation();
this.conditionalFields();
},
/**
* Tab Navigation
*/
tabs: function() {
// Tab click handler
$('.roi-tabs-nav a').on('click', function(e) {
e.preventDefault();
var tabId = $(this).attr('href');
// Update active states
$('.roi-tabs-nav li').removeClass('active');
$(this).parent().addClass('active');
// Show/hide tab content
$('.roi-tab-pane').removeClass('active');
$(tabId).addClass('active');
// Update URL hash without scrolling
if (history.pushState) {
history.pushState(null, null, tabId);
} else {
window.location.hash = tabId;
}
});
// Load tab from URL hash on page load
if (window.location.hash) {
var hash = window.location.hash;
if ($(hash).length) {
$('.roi-tabs-nav a[href="' + hash + '"]').trigger('click');
}
}
// Handle browser back/forward buttons
$(window).on('hashchange', function() {
if (window.location.hash) {
$('.roi-tabs-nav a[href="' + window.location.hash + '"]').trigger('click');
}
});
},
/**
* Image Upload
*/
imageUpload: function() {
var self = this;
var mediaUploader;
// Upload button click
$(document).on('click', '.roi-upload-image', function(e) {
e.preventDefault();
var button = $(this);
var container = button.closest('.roi-image-upload');
var preview = container.find('.roi-image-preview');
var input = container.find('.roi-image-id');
var removeBtn = container.find('.roi-remove-image');
// If the media uploader already exists, reopen it
if (mediaUploader) {
mediaUploader.open();
return;
}
// Create new media uploader
mediaUploader = wp.media({
title: roiAdminOptions.strings.selectImage,
button: {
text: roiAdminOptions.strings.useImage
},
multiple: false
});
// When an image is selected
mediaUploader.on('select', function() {
var attachment = mediaUploader.state().get('selection').first().toJSON();
// Set image ID
input.val(attachment.id);
// Show preview
var imgUrl = attachment.sizes && attachment.sizes.medium ?
attachment.sizes.medium.url : attachment.url;
preview.html('<img src="' + imgUrl + '" class="roi-preview-image" />');
// Show remove button
removeBtn.show();
});
// Open the uploader
mediaUploader.open();
});
// Remove button click
$(document).on('click', '.roi-remove-image', function(e) {
e.preventDefault();
var button = $(this);
var container = button.closest('.roi-image-upload');
var preview = container.find('.roi-image-preview');
var input = container.find('.roi-image-id');
// Clear values
input.val('');
preview.empty();
button.hide();
});
},
/**
* Reset Options
*/
resetOptions: function() {
$('#roi-reset-options').on('click', function(e) {
e.preventDefault();
if (!confirm(roiAdminOptions.strings.confirmReset)) {
return;
}
var button = $(this);
button.prop('disabled', true).addClass('updating-message');
$.ajax({
url: roiAdminOptions.ajaxUrl,
type: 'POST',
data: {
action: 'roi_reset_options',
nonce: roiAdminOptions.nonce
},
success: function(response) {
if (response.success) {
// Show success message
ROIThemeOptions.showNotice('success', response.data.message);
// Reload page after 1 second
setTimeout(function() {
window.location.reload();
}, 1000);
} else {
ROIThemeOptions.showNotice('error', response.data.message);
button.prop('disabled', false).removeClass('updating-message');
}
},
error: function() {
ROIThemeOptions.showNotice('error', roiAdminOptions.strings.error);
button.prop('disabled', false).removeClass('updating-message');
}
});
});
},
/**
* Export Options
*/
exportOptions: function() {
$('#roi-export-options').on('click', function(e) {
e.preventDefault();
var button = $(this);
button.prop('disabled', true).addClass('updating-message');
$.ajax({
url: roiAdminOptions.ajaxUrl,
type: 'POST',
data: {
action: 'roi_export_options',
nonce: roiAdminOptions.nonce
},
success: function(response) {
if (response.success) {
// Create download link
var blob = new Blob([response.data.data], { type: 'application/json' });
var url = window.URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = response.data.filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
ROIThemeOptions.showNotice('success', 'Options exported successfully!');
} else {
ROIThemeOptions.showNotice('error', response.data.message);
}
button.prop('disabled', false).removeClass('updating-message');
},
error: function() {
ROIThemeOptions.showNotice('error', roiAdminOptions.strings.error);
button.prop('disabled', false).removeClass('updating-message');
}
});
});
},
/**
* Import Options
*/
importOptions: function() {
var modal = $('#roi-import-modal');
var importData = $('#roi-import-data');
// Show modal
$('#roi-import-options').on('click', function(e) {
e.preventDefault();
modal.show();
});
// Close modal
$('.roi-modal-close, #roi-import-cancel').on('click', function() {
modal.hide();
importData.val('');
});
// Close modal on outside click
$(window).on('click', function(e) {
if ($(e.target).is(modal)) {
modal.hide();
importData.val('');
}
});
// Submit import
$('#roi-import-submit').on('click', function(e) {
e.preventDefault();
var data = importData.val().trim();
if (!data) {
alert('Please paste your import data.');
return;
}
var button = $(this);
button.prop('disabled', true).addClass('updating-message');
$.ajax({
url: roiAdminOptions.ajaxUrl,
type: 'POST',
data: {
action: 'roi_import_options',
nonce: roiAdminOptions.nonce,
import_data: data
},
success: function(response) {
if (response.success) {
ROIThemeOptions.showNotice('success', response.data.message);
modal.hide();
importData.val('');
// Reload page after 1 second
setTimeout(function() {
window.location.reload();
}, 1000);
} else {
ROIThemeOptions.showNotice('error', response.data.message);
button.prop('disabled', false).removeClass('updating-message');
}
},
error: function() {
ROIThemeOptions.showNotice('error', roiAdminOptions.strings.error);
button.prop('disabled', false).removeClass('updating-message');
}
});
});
},
/**
* Form Validation
*/
formValidation: function() {
$('.roi-options-form').on('submit', function(e) {
var valid = true;
var firstError = null;
// Validate required fields
$(this).find('[required]').each(function() {
if (!$(this).val()) {
valid = false;
$(this).addClass('error');
if (!firstError) {
firstError = $(this);
}
} else {
$(this).removeClass('error');
}
});
// Validate number fields
$(this).find('input[type="number"]').each(function() {
var val = $(this).val();
var min = $(this).attr('min');
var max = $(this).attr('max');
if (val && min && parseInt(val) < parseInt(min)) {
valid = false;
$(this).addClass('error');
if (!firstError) {
firstError = $(this);
}
}
if (val && max && parseInt(val) > parseInt(max)) {
valid = false;
$(this).addClass('error');
if (!firstError) {
firstError = $(this);
}
}
});
// Validate URL fields
$(this).find('input[type="url"]').each(function() {
var val = $(this).val();
if (val && !ROIThemeOptions.isValidUrl(val)) {
valid = false;
$(this).addClass('error');
if (!firstError) {
firstError = $(this);
}
}
});
if (!valid) {
e.preventDefault();
if (firstError) {
// Scroll to first error
$('html, body').animate({
scrollTop: firstError.offset().top - 100
}, 500);
firstError.focus();
}
ROIThemeOptions.showNotice('error', 'Please fix the errors in the form.');
return false;
}
// Add saving animation
$(this).find('.submit .button-primary').addClass('updating-message');
});
// Remove error class on input
$('.roi-options-form input, .roi-options-form select, .roi-options-form textarea').on('change input', function() {
$(this).removeClass('error');
});
},
/**
* Conditional Fields
*/
conditionalFields: function() {
// Enable/disable related posts options based on checkbox
$('#enable_related_posts').on('change', function() {
var checked = $(this).is(':checked');
var fields = $('#related_posts_count, #related_posts_taxonomy, #related_posts_title, #related_posts_columns');
fields.closest('tr').toggleClass('roi-field-dependency', !checked);
fields.prop('disabled', !checked);
}).trigger('change');
// Enable/disable breadcrumb separator based on breadcrumbs checkbox
$('#enable_breadcrumbs').on('change', function() {
var checked = $(this).is(':checked');
var field = $('#breadcrumb_separator');
field.closest('tr').toggleClass('roi-field-dependency', !checked);
field.prop('disabled', !checked);
}).trigger('change');
},
/**
* Show Notice
*/
showNotice: function(type, message) {
var notice = $('<div class="notice notice-' + type + ' is-dismissible"><p>' + message + '</p></div>');
$('.roi-theme-options h1').after(notice);
// Auto-dismiss after 5 seconds
setTimeout(function() {
notice.fadeOut(function() {
$(this).remove();
});
}, 5000);
// Scroll to top
$('html, body').animate({ scrollTop: 0 }, 300);
},
/**
* Validate URL
*/
isValidUrl: function(url) {
try {
new URL(url);
return true;
} catch (e) {
return false;
}
}
};
// Initialize on document ready
$(document).ready(function() {
ROIThemeOptions.init();
});
// Make it globally accessible
window.ROIThemeOptions = ROIThemeOptions;
})(jQuery);

View File

@@ -1,205 +0,0 @@
<?php
/**
* Admin Menu Class
*
* Registra menú en WordPress admin y carga assets
*
* @package ROI_Theme
* @since 2.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
class ROI_Admin_Menu {
/**
* Constructor
*/
public function __construct() {
add_action('admin_menu', array($this, 'register_menu'));
add_action('admin_enqueue_scripts', array($this, 'enqueue_assets'));
}
/**
* Registrar página de admin
* Crea menú de nivel superior en sidebar (NO dentro de Apariencia)
*/
public function register_menu() {
// Menú principal de nivel superior (sin callback para que sea solo contenedor)
add_menu_page(
'ROI Theme', // Page title
'ROI Theme', // Menu title
'manage_options', // Capability
'roi-theme-menu', // Menu slug (solo identificador, no página real)
'', // Sin callback = solo contenedor
'dashicons-admin-generic', // Icon (WordPress Dashicon)
61 // Position (61 = después de Appearance que es 60)
);
// Submenú 1: "Theme Options" (formulario viejo de apariencia)
add_submenu_page(
'roi-theme-menu', // Parent slug
'Theme Options', // Page title
'Theme Options', // Menu title
'manage_options', // Capability
'roi-theme-settings', // Menu slug
array($this, 'render_admin_page') // Callback
);
// Submenú 2: "Componentes" (nuevo sistema de tabs)
add_submenu_page(
'roi-theme-menu', // Parent slug
'Componentes', // Page title
'Componentes', // Menu title
'manage_options', // Capability
'roi-theme-components', // Menu slug
array($this, 'render_components_page') // Callback
);
// Remover el primer submenú duplicado que WordPress crea automáticamente
remove_submenu_page('roi-theme-menu', 'roi-theme-menu');
}
/**
* Renderizar página de Theme Options (formulario viejo)
*/
public function render_admin_page() {
if (!current_user_can('manage_options')) {
wp_die(__('No tienes permisos para acceder a esta página.'));
}
// Cargar el formulario viejo de theme options
require_once get_template_directory() . '/admin/theme-options/options-page-template.php';
}
/**
* Renderizar página de Componentes (nuevo sistema de tabs)
*/
public function render_components_page() {
if (!current_user_can('manage_options')) {
wp_die(__('No tienes permisos para acceder a esta página.'));
}
// Cargar el nuevo admin panel con tabs de componentes
require_once ROI_ADMIN_PANEL_PATH . 'pages/main.php';
}
/**
* Encolar assets (CSS/JS)
*/
public function enqueue_assets($hook) {
// Solo cargar en nuestras páginas de admin
$allowed_hooks = array(
'roi-theme_page_roi-theme-settings', // Theme Options
'roi-theme_page_roi-theme-components' // Componentes
);
if (!in_array($hook, $allowed_hooks)) {
return;
}
// CSS y JS específico para Theme Options (formulario viejo)
if ($hook === 'roi-theme_page_roi-theme-settings') {
// Enqueue WordPress media uploader
wp_enqueue_media();
// Enqueue admin styles para theme options
wp_enqueue_style(
'roi-admin-options',
get_template_directory_uri() . '/admin/assets/css/theme-options.css',
array(),
ROI_VERSION
);
// Enqueue admin scripts para theme options
wp_enqueue_script(
'roi-admin-options',
get_template_directory_uri() . '/admin/assets/js/theme-options.js',
array('jquery', 'wp-color-picker'),
ROI_VERSION,
true
);
// Localize script
wp_localize_script('roi-admin-options', 'roiAdminOptions', array(
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('roi_admin_nonce'),
'strings' => array(
'selectImage' => __('Select Image', 'roi-theme'),
'useImage' => __('Use Image', 'roi-theme'),
'removeImage' => __('Remove Image', 'roi-theme'),
'confirmReset' => __('Are you sure you want to reset all options to default values? This cannot be undone.', 'roi-theme'),
'saved' => __('Settings saved successfully!', 'roi-theme'),
'error' => __('An error occurred while saving settings.', 'roi-theme'),
),
));
// No cargar Bootstrap ni otros assets del nuevo panel
return;
}
// Bootstrap 5.3.2 CSS (solo para Componentes)
wp_enqueue_style(
'bootstrap',
'https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css',
array(),
'5.3.2'
);
// Bootstrap Icons
wp_enqueue_style(
'bootstrap-icons',
'https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css',
array(),
'1.11.1'
);
// Admin Panel CSS (Core)
wp_enqueue_style(
'roi-admin-panel-css',
ROI_ADMIN_PANEL_URL . 'assets/css/admin-panel.css',
array('bootstrap'),
ROI_ADMIN_PANEL_VERSION
);
// Bootstrap 5.3.2 JS
wp_enqueue_script(
'bootstrap',
'https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js',
array(),
'5.3.2',
true
);
// Axios (para AJAX)
wp_enqueue_script(
'axios',
'https://cdn.jsdelivr.net/npm/axios@1.6.0/dist/axios.min.js',
array(),
'1.6.0',
true
);
// Admin Panel JS (Core)
wp_enqueue_script(
'roi-admin-panel-js',
ROI_ADMIN_PANEL_URL . 'assets/js/admin-app.js',
array('jquery', 'axios'),
ROI_ADMIN_PANEL_VERSION,
true
);
// Pasar datos a JavaScript
wp_localize_script('roi-admin-panel-js', 'roiAdminData', array(
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('roi_admin_nonce')
));
}
}
// Instanciar clase
new ROI_Admin_Menu();

View File

@@ -1,318 +0,0 @@
<?php
/**
* Database Manager Class
*
* Gestión de tablas personalizadas del tema
*
* @package ROI_Theme
* @since 2.2.0
* @deprecated 2.0.0 Use ROITheme\Infrastructure\Persistence\WordPress\WordPressComponentRepository instead
* @see ROITheme\Infrastructure\Persistence\WordPress\WordPressComponentRepository
*
* Esta clase será eliminada en la versión 3.0.0
* Por favor migre su código a la nueva arquitectura Clean Architecture
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* ROI_DB_Manager
*
* @deprecated 2.0.0
*/
class ROI_DB_Manager {
/**
* Nombre de la tabla de componentes (sin prefijo)
*
* @deprecated 2.0.0
*/
const TABLE_COMPONENTS = 'roi_theme_components';
/**
* Nombre de la tabla de defaults (sin prefijo)
*
* @deprecated 2.0.0
*/
const TABLE_DEFAULTS = 'roi_theme_components_defaults';
/**
* Versión de la base de datos
*
* @deprecated 2.0.0
*/
const DB_VERSION = '1.0';
/**
* Opción para almacenar la versión de la DB
*
* @deprecated 2.0.0
*/
const DB_VERSION_OPTION = 'roi_db_version';
/**
* @var \ROITheme\Infrastructure\Adapters\LegacyDBManagerAdapter
*/
private $adapter;
/**
* Constructor
*
* @deprecated 2.0.0
*/
public function __construct() {
_deprecated_function(
__CLASS__ . '::__construct',
'2.0.0',
'ROITheme\Infrastructure\Persistence\WordPress\WordPressComponentRepository'
);
$this->logDeprecation(__CLASS__, __FUNCTION__);
// Hook para verificar/actualizar DB en cada carga
add_action('admin_init', array($this, 'maybe_create_tables'));
// Inicializar adapter para mantener compatibilidad
$this->adapter = $this->getLegacyAdapter();
}
/**
* Obtener nombre completo de tabla con prefijo
*
* @deprecated 2.0.0
*
* @param string $table_type Tipo de tabla: 'components' (personalizaciones) o 'defaults' (valores por defecto)
* @return string Nombre completo de la tabla con prefijo
*/
public function get_table_name($table_type = 'components') {
_deprecated_function(
__FUNCTION__,
'2.0.0',
'Direct database access not recommended - use repositories'
);
global $wpdb;
if ($table_type === 'defaults') {
return $wpdb->prefix . self::TABLE_DEFAULTS;
}
return $wpdb->prefix . self::TABLE_COMPONENTS;
}
/**
* Verificar si las tablas necesitan ser creadas o actualizadas
*
* @deprecated 2.0.0 Tables are now managed through database migrations
*/
public function maybe_create_tables() {
// Keep for backward compatibility but tables are managed differently now
$installed_version = get_option(self::DB_VERSION_OPTION);
if ($installed_version !== self::DB_VERSION) {
$this->create_tables();
update_option(self::DB_VERSION_OPTION, self::DB_VERSION);
}
}
/**
* Crear tablas personalizadas
*
* @deprecated 2.0.0 Use DatabaseMigrator service instead
*/
public function create_tables() {
_deprecated_function(
__FUNCTION__,
'2.0.0',
'ROITheme\Infrastructure\Services\DatabaseMigrator'
);
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
// Table structure (kept for backward compatibility)
$table_structure = "(
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
component_name VARCHAR(50) NOT NULL,
config_key VARCHAR(100) NOT NULL,
config_value TEXT NOT NULL,
data_type ENUM('string', 'boolean', 'integer', 'json') DEFAULT 'string',
version VARCHAR(10) DEFAULT NULL,
updated_at DATETIME NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY component_config (component_name, config_key),
INDEX idx_component (component_name),
INDEX idx_updated (updated_at)
) $charset_collate;";
// Create components table
$table_components = $this->get_table_name('components');
$sql_components = "CREATE TABLE IF NOT EXISTS $table_components $table_structure";
dbDelta($sql_components);
// Create defaults table
$table_defaults = $this->get_table_name('defaults');
$sql_defaults = "CREATE TABLE IF NOT EXISTS $table_defaults $table_structure";
dbDelta($sql_defaults);
}
/**
* Verificar si una tabla existe
*
* @deprecated 2.0.0
*
* @param string $table_type Tipo de tabla
* @return bool True si existe, false si no
*/
public function table_exists($table_type = 'components') {
global $wpdb;
$table_name = $this->get_table_name($table_type);
$query = $wpdb->prepare('SHOW TABLES LIKE %s', $table_name);
return $wpdb->get_var($query) === $table_name;
}
/**
* Save component configuration
*
* @deprecated 2.0.0 Use SaveComponentUseCase::execute() instead
*
* @param string $component_name Component name (e.g., 'top_notification_bar', 'navbar', 'footer', 'hero_section')
* @param string $config_key Configuration key
* @param mixed $config_value Configuration value
* @param string $data_type Data type (string, boolean, integer, array)
* @param string|null $version Schema version
* @param string $table_type Table type (components or defaults)
* @return bool Success status
*/
public function save_config($component_name, $config_key, $config_value, $data_type = 'string', $version = null, $table_type = 'components') {
_deprecated_function(
__FUNCTION__,
'2.0.0',
'ROITheme\Application\UseCases\SaveComponent\SaveComponentUseCase::execute()'
);
$this->logDeprecation(__CLASS__, __FUNCTION__, func_get_args());
// Delegar al adapter para mantener funcionalidad
return $this->adapter->save_config(
$component_name,
$config_key,
$config_value,
$data_type,
$version,
$table_type
);
}
/**
* Get component configuration
*
* @deprecated 2.0.0 Use GetComponentUseCase::execute() instead
*
* @param string $component_name Component name
* @param string|null $config_key Specific configuration key (null for all)
* @param string $table_type Table type (components or defaults)
* @return mixed Configuration value(s) or null
*/
public function get_config($component_name, $config_key = null, $table_type = 'components') {
_deprecated_function(
__FUNCTION__,
'2.0.0',
'ROITheme\Application\UseCases\GetComponent\GetComponentUseCase::execute()'
);
$this->logDeprecation(__CLASS__, __FUNCTION__, func_get_args());
return $this->adapter->get_config($component_name, $config_key, $table_type);
}
/**
* Delete component configuration
*
* @deprecated 2.0.0 Use DeleteComponentUseCase::execute() instead
*
* @param string $component_name Component name
* @param string|null $config_key Specific configuration key (null for all component)
* @param string $table_type Table type (components or defaults)
* @return bool Success status
*/
public function delete_config($component_name, $config_key = null, $table_type = 'components') {
_deprecated_function(
__FUNCTION__,
'2.0.0',
'ROITheme\Application\UseCases\DeleteComponent\DeleteComponentUseCase::execute()'
);
$this->logDeprecation(__CLASS__, __FUNCTION__, func_get_args());
// If deleting specific key, not supported in new architecture
// Delete entire component instead
return $this->adapter->delete_config($component_name, $table_type);
}
/**
* List all components
*
* @deprecated 2.0.0 Use ComponentRepository::findAll() instead
*
* @param string $table_type Table type
* @return array List of components
*/
public function list_components($table_type = 'components') {
_deprecated_function(
__FUNCTION__,
'2.0.0',
'ROITheme\Domain\Contracts\ComponentRepositoryInterface::findAll()'
);
$this->logDeprecation(__CLASS__, __FUNCTION__, func_get_args());
global $wpdb;
$table = $this->get_table_name($table_type);
$query = "SELECT DISTINCT component_name FROM $table ORDER BY component_name ASC";
$components = $wpdb->get_col($query);
return $components ?: array();
}
/**
* Get legacy adapter
*
* @return \ROITheme\Infrastructure\Adapters\LegacyDBManagerAdapter
*/
private function getLegacyAdapter() {
if (!class_exists('ROITheme\Infrastructure\Adapters\LegacyDBManagerAdapter')) {
require_once get_template_directory() . '/vendor/autoload.php';
}
return new \ROITheme\Infrastructure\Adapters\LegacyDBManagerAdapter();
}
/**
* Log deprecation usage
*
* @param string $class Class name
* @param string $method Method name
* @param array $args Arguments
*/
private function logDeprecation($class, $method, $args = array()) {
if (!class_exists('ROITheme\Infrastructure\Logging\DeprecationLogger')) {
require_once get_template_directory() . '/vendor/autoload.php';
}
$logger = \ROITheme\Infrastructure\Logging\DeprecationLogger::getInstance();
$logger->log(
$class,
$method,
$args,
'See documentation for Clean Architecture migration',
'2.0.0'
);
}
}

View File

@@ -1,156 +0,0 @@
<?php
/**
* Settings Manager Class
*
* CRUD de configuraciones por componentes
*
* @package ROI_Theme
* @since 2.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
class ROI_Settings_Manager {
const OPTION_NAME = 'roi_theme_settings';
/**
* Constructor
*/
public function __construct() {
add_action('wp_ajax_roi_get_settings', array($this, 'ajax_get_settings'));
add_action('wp_ajax_roi_save_settings', array($this, 'ajax_save_settings'));
}
/**
* Obtener configuraciones
*/
public function get_settings() {
$settings = get_option(self::OPTION_NAME, array());
$defaults = $this->get_defaults();
return wp_parse_args($settings, $defaults);
}
/**
* Guardar configuraciones
*/
public function save_settings($data) {
// Validar
$validator = new ROI_Validator();
$validation = $validator->validate($data);
if (!$validation['valid']) {
return array(
'success' => false,
'message' => 'Error de validación',
'errors' => $validation['errors']
);
}
// Sanitizar
$sanitized = $this->sanitize_settings($data);
// Agregar metadata
$sanitized['version'] = ROI_ADMIN_PANEL_VERSION;
$sanitized['updated_at'] = current_time('mysql');
// Guardar
update_option(self::OPTION_NAME, $sanitized, false);
return array(
'success' => true,
'message' => 'Configuración guardada correctamente'
);
}
/**
* Valores por defecto
* Lee los defaults desde la tabla wp_roi_theme_components_defaults
*/
public function get_defaults() {
$db_manager = new ROI_DB_Manager();
$component_names = $db_manager->list_components('defaults');
$defaults = array(
'version' => ROI_ADMIN_PANEL_VERSION,
'components' => array()
);
// Obtener configuraciones de cada componente desde la tabla de defaults
foreach ($component_names as $component_name) {
$defaults['components'][$component_name] = $db_manager->get_config($component_name, null, 'defaults');
}
return $defaults;
}
/**
* Sanitizar configuraciones
* NOTA: Los sanitizers de componentes se ejecutarán aquí cuando se implementen
*/
public function sanitize_settings($data) {
$sanitized = array(
'components' => array()
);
// Los componentes se sanitizarán aquí cuando se ejecute el algoritmo
return $sanitized;
}
/**
* AJAX: Obtener configuraciones
*/
public function ajax_get_settings() {
// Verificar nonce usando check_ajax_referer (método recomendado para AJAX)
check_ajax_referer('roi_admin_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Permisos insuficientes');
}
$settings = $this->get_settings();
wp_send_json_success($settings);
}
/**
* AJAX: Guardar configuraciones
*/
public function ajax_save_settings() {
// Verificar nonce usando check_ajax_referer (método recomendado para AJAX)
check_ajax_referer('roi_admin_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Permisos insuficientes');
}
// Los datos vienen como JSON string en $_POST['components']
if (!isset($_POST['components'])) {
wp_send_json_error('Datos inválidos - falta components');
}
$components = json_decode(stripslashes($_POST['components']), true);
if (!is_array($components)) {
wp_send_json_error('Datos inválidos - components no es un array válido');
}
$data = array(
'components' => $components
);
$result = $this->save_settings($data);
if ($result['success']) {
wp_send_json_success($result);
} else {
wp_send_json_error($result);
}
}
}
// Instanciar clase
new ROI_Settings_Manager();

View File

@@ -1,37 +0,0 @@
<?php
/**
* Validator Class
*
* Validación de datos por componentes
*
* @package ROI_Theme
* @since 2.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
class ROI_Validator {
/**
* Validar todas las configuraciones
* Los validators de componentes se ejecutarán aquí cuando se implementen
*/
public function validate($data) {
$errors = array();
// Validar estructura base
if (!isset($data['components']) || !is_array($data['components'])) {
$errors[] = 'Estructura de datos inválida';
return array('valid' => false, 'errors' => $errors);
}
// Los componentes se validarán aquí cuando se ejecute el algoritmo
return array(
'valid' => empty($errors),
'errors' => $errors
);
}
}

View File

@@ -1,271 +0,0 @@
<?php
/**
* Sanitizer Helper
*
* Métodos estáticos reutilizables para sanitización de datos
*
* @package ROI_Theme
* @subpackage Admin_Panel\Sanitizers
* @since 2.1.0
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Class ROI_Sanitizer_Helper
*
* Proporciona métodos estáticos para sanitización común,
* eliminando código duplicado en los sanitizadores de componentes
*/
class ROI_Sanitizer_Helper {
/**
* Sanitiza un valor booleano
*
* @param array $data Array de datos
* @param string $key Clave del dato
* @return bool Valor booleano sanitizado
*/
public static function sanitize_boolean($data, $key) {
return !empty($data[$key]);
}
/**
* Sanitiza múltiples valores booleanos
*
* @param array $data Array de datos
* @param array $keys Array de claves a sanitizar
* @return array Array asociativo con valores booleanos sanitizados
*/
public static function sanitize_booleans($data, $keys) {
$result = array();
foreach ($keys as $key) {
$result[$key] = self::sanitize_boolean($data, $key);
}
return $result;
}
/**
* Sanitiza un campo de texto con valor por defecto
*
* @param array $data Array de datos
* @param string $key Clave del dato
* @param string $default Valor por defecto (default: '')
* @return string Texto sanitizado
*/
public static function sanitize_text($data, $key, $default = '') {
return sanitize_text_field($data[$key] ?? $default);
}
/**
* Sanitiza múltiples campos de texto
*
* @param array $data Array de datos
* @param array $keys Array de claves a sanitizar
* @param string $default Valor por defecto para todos (default: '')
* @return array Array asociativo con textos sanitizados
*/
public static function sanitize_texts($data, $keys, $default = '') {
$result = array();
foreach ($keys as $key) {
$result[$key] = self::sanitize_text($data, $key, $default);
}
return $result;
}
/**
* Sanitiza un color hexadecimal con valor por defecto
*
* @param array $data Array de datos
* @param string $key Clave del dato
* @param string $default Valor por defecto (default: '')
* @return string Color hexadecimal sanitizado
*/
public static function sanitize_color($data, $key, $default = '') {
return sanitize_hex_color($data[$key] ?? $default);
}
/**
* Sanitiza múltiples colores hexadecimales
*
* @param array $data Array de datos
* @param array $keys Array de claves a sanitizar
* @param string $default Valor por defecto para todos (default: '')
* @return array Array asociativo con colores sanitizados
*/
public static function sanitize_colors($data, $keys, $default = '') {
$result = array();
foreach ($keys as $key) {
$result[$key] = self::sanitize_color($data, $key, $default);
}
return $result;
}
/**
* Sanitiza un valor con validación enum (in_array)
*
* @param array $data Array de datos
* @param string $key Clave del dato
* @param array $allowed_values Valores permitidos
* @param mixed $default Valor por defecto
* @return mixed Valor sanitizado
*/
public static function sanitize_enum($data, $key, $allowed_values, $default) {
return in_array($data[$key] ?? '', $allowed_values, true)
? $data[$key]
: $default;
}
/**
* Sanitiza múltiples valores enum
*
* @param array $data Array de datos
* @param array $config Array de configuración [key => ['allowed' => [...], 'default' => ...]]
* @return array Array asociativo con valores enum sanitizados
*/
public static function sanitize_enums($data, $config) {
$result = array();
foreach ($config as $key => $settings) {
$result[$key] = self::sanitize_enum(
$data,
$key,
$settings['allowed'],
$settings['default']
);
}
return $result;
}
/**
* Sanitiza un valor entero con valor por defecto
*
* @param array $data Array de datos
* @param string $key Clave del dato
* @param int $default Valor por defecto
* @return int Entero sanitizado
*/
public static function sanitize_int($data, $key, $default = 0) {
return isset($data[$key]) ? intval($data[$key]) : $default;
}
/**
* Sanitiza múltiples valores enteros
*
* @param array $data Array de datos
* @param array $config Array de configuración [key => default_value]
* @return array Array asociativo con enteros sanitizados
*/
public static function sanitize_ints($data, $config) {
$result = array();
foreach ($config as $key => $default) {
$result[$key] = self::sanitize_int($data, $key, $default);
}
return $result;
}
/**
* Sanitiza un valor float con valor por defecto
*
* @param array $data Array de datos
* @param string $key Clave del dato
* @param float $default Valor por defecto
* @return float Float sanitizado
*/
public static function sanitize_float($data, $key, $default = 0.0) {
return isset($data[$key]) ? floatval($data[$key]) : $default;
}
/**
* Sanitiza múltiples valores float
*
* @param array $data Array de datos
* @param array $config Array de configuración [key => default_value]
* @return array Array asociativo con floats sanitizados
*/
public static function sanitize_floats($data, $config) {
$result = array();
foreach ($config as $key => $default) {
$result[$key] = self::sanitize_float($data, $key, $default);
}
return $result;
}
/**
* Sanitiza una URL con valor por defecto
*
* @param array $data Array de datos
* @param string $key Clave del dato
* @param string $default Valor por defecto (default: '')
* @return string URL sanitizada
*/
public static function sanitize_url($data, $key, $default = '') {
return esc_url_raw($data[$key] ?? $default);
}
/**
* Sanitiza un array de strings
*
* @param array $data Array de datos
* @param string $key Clave del dato
* @param array $default Array por defecto
* @return array Array de strings sanitizados
*/
public static function sanitize_array_of_strings($data, $key, $default = array()) {
return isset($data[$key]) && is_array($data[$key])
? array_map('sanitize_text_field', $data[$key])
: $default;
}
/**
* Sanitiza un grupo de campos anidados (custom_styles, dropdown, etc.)
*
* @param array $data Array de datos completo
* @param string $group_key Clave del grupo (ej: 'custom_styles')
* @param array $sanitization_rules Reglas de sanitización por campo
* Formato: [
* 'campo' => ['type' => 'text|color|int|float|enum|bool', 'default' => valor, 'allowed' => array()]
* ]
* @return array Array con campos del grupo sanitizados
*/
public static function sanitize_nested_group($data, $group_key, $sanitization_rules) {
$result = array();
$group_data = $data[$group_key] ?? array();
foreach ($sanitization_rules as $field => $rule) {
$type = $rule['type'];
$default = $rule['default'] ?? null;
switch ($type) {
case 'text':
$result[$field] = self::sanitize_text($group_data, $field, $default ?? '');
break;
case 'color':
$result[$field] = self::sanitize_color($group_data, $field, $default ?? '');
break;
case 'int':
$result[$field] = self::sanitize_int($group_data, $field, $default ?? 0);
break;
case 'float':
$result[$field] = self::sanitize_float($group_data, $field, $default ?? 0.0);
break;
case 'enum':
$result[$field] = self::sanitize_enum(
$group_data,
$field,
$rule['allowed'] ?? array(),
$default
);
break;
case 'bool':
$result[$field] = self::sanitize_boolean($group_data, $field);
break;
default:
$result[$field] = $group_data[$field] ?? $default;
}
}
return $result;
}
}

View File

@@ -1,31 +0,0 @@
<?php
/**
* Admin Panel Module - Initialization
*
* Sistema de configuración por componentes
* Cada componente del tema es configurable desde el admin panel
*
* @package ROI_Theme
* @since 2.0.0
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
// Module constants
define('ROI_ADMIN_PANEL_VERSION', '2.1.4');
define('ROI_ADMIN_PANEL_PATH', get_template_directory() . '/admin/');
define('ROI_ADMIN_PANEL_URL', get_template_directory_uri() . '/admin/');
// Load classes
require_once ROI_ADMIN_PANEL_PATH . 'includes/class-admin-menu.php';
require_once ROI_ADMIN_PANEL_PATH . 'includes/class-db-manager.php';
require_once ROI_ADMIN_PANEL_PATH . 'includes/class-validator.php';
// Settings Manager
require_once ROI_ADMIN_PANEL_PATH . 'includes/class-settings-manager.php';
// Initialize Database Manager
new ROI_DB_Manager();

View File

@@ -1,34 +0,0 @@
<?php
/**
* Admin Panel - Main Page
*
* Interfaz de administración de componentes del tema
*
* @package ROI_Theme
* @since 2.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
?>
<div class="wrap roi-admin-panel">
<!-- Navigation Tabs -->
<ul class="nav nav-tabs" role="tablist">
<!-- Tabs de componentes se generarán aquí cuando se ejecute el algoritmo -->
</ul>
<!-- Tab Content -->
<div class="tab-content mt-3">
<!-- Contenido de tabs de componentes se generará aquí cuando se ejecute el algoritmo -->
</div>
<!-- Action Buttons -->
<div class="admin-actions mt-4">
<button type="button" id="saveSettings" class="button button-primary" disabled>
<i class="bi bi-save me-2"></i>Guardar Cambios
</button>
<span class="spinner" style="display: none; float: none; margin-left: 10px;"></span>
</div>
</div>

View File

@@ -1,394 +0,0 @@
<?php
/**
* Theme Options Usage Examples
*
* This file contains examples of how to use theme options throughout the theme.
* DO NOT include this file in functions.php - it's for reference only.
*
* @package ROI_Theme
* @since 1.0.0
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* EXAMPLE 1: Using options in header.php
*/
function example_display_logo() {
$logo_url = roi_get_logo_url();
if ($logo_url) {
?>
<a href="<?php echo esc_url(home_url('/')); ?>" class="custom-logo-link">
<img src="<?php echo esc_url($logo_url); ?>" alt="<?php bloginfo('name'); ?>" class="custom-logo" />
</a>
<?php
} else {
?>
<h1 class="site-title">
<a href="<?php echo esc_url(home_url('/')); ?>"><?php bloginfo('name'); ?></a>
</h1>
<?php
}
}
/**
* EXAMPLE 2: Displaying breadcrumbs
*/
function example_show_breadcrumbs() {
if (roi_show_breadcrumbs() && !is_front_page()) {
$separator = roi_get_breadcrumb_separator();
echo '<nav class="breadcrumbs">';
echo '<a href="' . esc_url(home_url('/')) . '">Home</a>';
echo ' ' . esc_html($separator) . ' ';
if (is_single()) {
the_category(' ' . esc_html($separator) . ' ');
echo ' ' . esc_html($separator) . ' ';
the_title();
} elseif (is_category()) {
single_cat_title();
}
echo '</nav>';
}
}
/**
* EXAMPLE 3: Customizing excerpt
*/
function example_custom_excerpt_length($length) {
return roi_get_excerpt_length();
}
add_filter('excerpt_length', 'example_custom_excerpt_length');
function example_custom_excerpt_more($more) {
return roi_get_excerpt_more();
}
add_filter('excerpt_more', 'example_custom_excerpt_more');
/**
* EXAMPLE 4: Displaying related posts in single.php
*/
function example_display_related_posts() {
if (roi_show_related_posts() && is_single()) {
$count = roi_get_related_posts_count();
$taxonomy = roi_get_related_posts_taxonomy();
$title = roi_get_related_posts_title();
// Get related posts
$post_id = get_the_ID();
$args = array(
'posts_per_page' => $count,
'post__not_in' => array($post_id),
);
if ($taxonomy === 'category') {
$categories = wp_get_post_categories($post_id);
if ($categories) {
$args['category__in'] = $categories;
}
} elseif ($taxonomy === 'tag') {
$tags = wp_get_post_tags($post_id, array('fields' => 'ids'));
if ($tags) {
$args['tag__in'] = $tags;
}
}
$related = new WP_Query($args);
if ($related->have_posts()) {
?>
<div class="related-posts">
<h3><?php echo esc_html($title); ?></h3>
<div class="related-posts-grid">
<?php
while ($related->have_posts()) {
$related->the_post();
?>
<article class="related-post-item">
<?php if (has_post_thumbnail()) : ?>
<a href="<?php the_permalink(); ?>">
<?php the_post_thumbnail('roi-thumbnail'); ?>
</a>
<?php endif; ?>
<h4>
<a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
</h4>
<div class="post-meta">
<time datetime="<?php echo get_the_date('c'); ?>">
<?php echo get_the_date(roi_get_date_format()); ?>
</time>
</div>
</article>
<?php
}
wp_reset_postdata();
?>
</div>
</div>
<?php
}
}
}
/**
* EXAMPLE 5: Conditional comments display
*/
function example_maybe_show_comments() {
if (is_single() && roi_comments_enabled_for_posts()) {
comments_template();
} elseif (is_page() && roi_comments_enabled_for_pages()) {
comments_template();
}
}
/**
* EXAMPLE 6: Featured image on single posts
*/
function example_display_featured_image() {
if (is_single() && roi_show_featured_image_single() && has_post_thumbnail()) {
?>
<div class="post-thumbnail">
<?php the_post_thumbnail('roi-featured-large'); ?>
</div>
<?php
}
}
/**
* EXAMPLE 7: Author box on single posts
*/
function example_display_author_box() {
if (is_single() && roi_show_author_box()) {
$author_id = get_the_author_meta('ID');
?>
<div class="author-box">
<div class="author-avatar">
<?php echo get_avatar($author_id, 80); ?>
</div>
<div class="author-info">
<h4 class="author-name"><?php the_author(); ?></h4>
<p class="author-bio"><?php the_author_meta('description'); ?></p>
<a href="<?php echo get_author_posts_url($author_id); ?>" class="author-link">
<?php _e('View all posts', 'roi-theme'); ?>
</a>
</div>
</div>
<?php
}
}
/**
* EXAMPLE 8: Social media links in footer
*/
function example_display_social_links() {
$social_links = roi_get_social_links();
// Filter out empty links
$social_links = array_filter($social_links);
if (!empty($social_links)) {
?>
<div class="social-links">
<?php foreach ($social_links as $network => $url) : ?>
<a href="<?php echo esc_url($url); ?>"
target="_blank"
rel="noopener noreferrer"
class="social-link social-<?php echo esc_attr($network); ?>">
<span class="screen-reader-text"><?php echo ucfirst($network); ?></span>
<i class="icon-<?php echo esc_attr($network); ?>"></i>
</a>
<?php endforeach; ?>
</div>
<?php
}
}
/**
* EXAMPLE 9: Copyright text in footer
*/
function example_display_copyright() {
$copyright = roi_get_copyright_text();
if ($copyright) {
echo '<div class="copyright">' . wp_kses_post($copyright) . '</div>';
}
}
/**
* EXAMPLE 10: Custom CSS in header
*/
function example_add_custom_css() {
$custom_css = roi_get_custom_css();
if ($custom_css) {
echo '<style type="text/css">' . "\n";
echo strip_tags($custom_css);
echo "\n</style>\n";
}
}
add_action('wp_head', 'example_add_custom_css', 100);
/**
* EXAMPLE 11: Custom JS in header
*/
function example_add_custom_js_header() {
$custom_js = roi_get_custom_js_header();
if ($custom_js) {
echo '<script type="text/javascript">' . "\n";
echo $custom_js;
echo "\n</script>\n";
}
}
add_action('wp_head', 'example_add_custom_js_header', 100);
/**
* EXAMPLE 12: Custom JS in footer
*/
function example_add_custom_js_footer() {
$custom_js = roi_get_custom_js_footer();
if ($custom_js) {
echo '<script type="text/javascript">' . "\n";
echo $custom_js;
echo "\n</script>\n";
}
}
add_action('wp_footer', 'example_add_custom_js_footer', 100);
/**
* EXAMPLE 13: Posts per page for archives
*/
function example_set_archive_posts_per_page($query) {
if ($query->is_archive() && !is_admin() && $query->is_main_query()) {
$posts_per_page = roi_get_archive_posts_per_page();
$query->set('posts_per_page', $posts_per_page);
}
}
add_action('pre_get_posts', 'example_set_archive_posts_per_page');
/**
* EXAMPLE 14: Performance optimizations
*/
function example_apply_performance_settings() {
// Remove emoji scripts
if (roi_is_performance_enabled('remove_emoji')) {
remove_action('wp_head', 'print_emoji_detection_script', 7);
remove_action('wp_print_styles', 'print_emoji_styles');
}
// Remove embeds
if (roi_is_performance_enabled('remove_embeds')) {
wp_deregister_script('wp-embed');
}
// Remove Dashicons for non-logged users
if (roi_is_performance_enabled('remove_dashicons') && !is_user_logged_in()) {
wp_deregister_style('dashicons');
}
}
add_action('wp_enqueue_scripts', 'example_apply_performance_settings', 100);
/**
* EXAMPLE 15: Lazy loading images
*/
function example_add_lazy_loading($attr, $attachment, $size) {
if (roi_is_lazy_loading_enabled()) {
$attr['loading'] = 'lazy';
}
return $attr;
}
add_filter('wp_get_attachment_image_attributes', 'example_add_lazy_loading', 10, 3);
/**
* EXAMPLE 16: Layout classes based on settings
*/
function example_get_layout_class() {
$layout = 'right-sidebar'; // default
if (is_single()) {
$layout = roi_get_default_post_layout();
} elseif (is_page()) {
$layout = roi_get_default_page_layout();
}
return 'layout-' . $layout;
}
/**
* EXAMPLE 17: Display post meta conditionally
*/
function example_display_post_meta() {
if (!roi_get_option('show_post_meta', true)) {
return;
}
?>
<div class="post-meta">
<span class="post-date">
<time datetime="<?php echo get_the_date('c'); ?>">
<?php echo get_the_date(roi_get_date_format()); ?>
</time>
</span>
<span class="post-author">
<?php the_author(); ?>
</span>
<?php if (roi_get_option('show_post_categories', true)) : ?>
<span class="post-categories">
<?php the_category(', '); ?>
</span>
<?php endif; ?>
</div>
<?php
}
/**
* EXAMPLE 18: Display post tags conditionally
*/
function example_display_post_tags() {
if (is_single() && roi_get_option('show_post_tags', true)) {
the_tags('<div class="post-tags">', ', ', '</div>');
}
}
/**
* EXAMPLE 19: Get all options (for debugging)
*/
function example_debug_all_options() {
if (current_user_can('manage_options') && isset($_GET['debug_options'])) {
$all_options = roi_get_all_options();
echo '<pre>';
print_r($all_options);
echo '</pre>';
}
}
add_action('wp_footer', 'example_debug_all_options');
/**
* EXAMPLE 20: Check if specific feature is enabled
*/
function example_check_feature() {
// Multiple ways to check boolean options
// Method 1: Using helper function
if (roi_is_option_enabled('enable_breadcrumbs')) {
// Breadcrumbs are enabled
}
// Method 2: Using get_option with default
if (roi_get_option('enable_related_posts', true)) {
// Related posts are enabled
}
// Method 3: Direct check
$options = roi_get_all_options();
if (isset($options['enable_lazy_loading']) && $options['enable_lazy_loading']) {
// Lazy loading is enabled
}
}

View File

@@ -1,237 +0,0 @@
<?php
/**
* Theme Options Settings API
*
* @package ROI_Theme
* @since 1.0.0
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Register all theme settings
*/
function roi_register_settings() {
// Register main options group
register_setting(
'roi_theme_options_group',
'roi_theme_options',
array(
'sanitize_callback' => 'roi_sanitize_options',
'default' => roi_get_default_options(),
)
);
// General Settings Section
add_settings_section(
'roi_general_section',
__('General Settings', 'roi-theme'),
'roi_general_section_callback',
'roitheme-options'
);
// Content Settings Section
add_settings_section(
'roi_content_section',
__('Content Settings', 'roi-theme'),
'roi_content_section_callback',
'roitheme-options'
);
// Performance Settings Section
add_settings_section(
'roi_performance_section',
__('Performance Settings', 'roi-theme'),
'roi_performance_section_callback',
'roitheme-options'
);
// Related Posts Settings Section
add_settings_section(
'roi_related_posts_section',
__('Related Posts Settings', 'roi-theme'),
'roi_related_posts_section_callback',
'roitheme-options'
);
// Social Share Settings Section
add_settings_section(
'roi_social_share_section',
__('Social Share Buttons', 'roi-theme'),
'roi_social_share_section_callback',
'roitheme-options'
);
}
add_action('admin_init', 'roi_register_settings');
/**
* Get default options
*
* @return array
*/
function roi_get_default_options() {
return array(
// General
'site_logo' => 0,
'site_favicon' => 0,
'enable_breadcrumbs' => true,
'breadcrumb_separator' => '>',
'date_format' => 'd/m/Y',
'time_format' => 'H:i',
'copyright_text' => sprintf(__('&copy; %s %s. All rights reserved.', 'roi-theme'), date('Y'), get_bloginfo('name')),
'social_facebook' => '',
'social_twitter' => '',
'social_instagram' => '',
'social_linkedin' => '',
'social_youtube' => '',
// Content
'excerpt_length' => 55,
'excerpt_more' => '...',
'default_post_layout' => 'right-sidebar',
'default_page_layout' => 'right-sidebar',
'archive_posts_per_page' => 10,
'show_featured_image_single' => true,
'show_author_box' => true,
'enable_comments_posts' => true,
'enable_comments_pages' => false,
'show_post_meta' => true,
'show_post_tags' => true,
'show_post_categories' => true,
// Performance
'enable_lazy_loading' => true,
'performance_remove_emoji' => true,
'performance_remove_embeds' => false,
'performance_remove_dashicons' => true,
'performance_defer_js' => false,
'performance_minify_html' => false,
'performance_disable_gutenberg' => false,
// Related Posts
'enable_related_posts' => true,
'related_posts_count' => 3,
'related_posts_taxonomy' => 'category',
'related_posts_title' => __('Related Posts', 'roi-theme'),
'related_posts_columns' => 3,
// Social Share Buttons
'roi_enable_share_buttons' => '1',
'roi_share_text' => __('Compartir:', 'roi-theme'),
// Advanced
'custom_css' => '',
'custom_js_header' => '',
'custom_js_footer' => '',
);
}
/**
* Section Callbacks
*/
function roi_general_section_callback() {
echo '<p>' . __('Configure general theme settings including logo, branding, and social media.', 'roi-theme') . '</p>';
}
function roi_content_section_callback() {
echo '<p>' . __('Configure content display settings for posts, pages, and archives.', 'roi-theme') . '</p>';
}
function roi_performance_section_callback() {
echo '<p>' . __('Optimize your site performance with these settings.', 'roi-theme') . '</p>';
}
function roi_related_posts_section_callback() {
echo '<p>' . __('Configure related posts display on single post pages.', 'roi-theme') . '</p>';
}
function roi_social_share_section_callback() {
echo '<p>' . __('Configure social share buttons display on single post pages.', 'roi-theme') . '</p>';
}
/**
* Sanitize all options
*
* @param array $input The input array
* @return array The sanitized array
*/
function roi_sanitize_options($input) {
$sanitized = array();
if (!is_array($input)) {
return $sanitized;
}
// General Settings
$sanitized['site_logo'] = isset($input['site_logo']) ? absint($input['site_logo']) : 0;
$sanitized['site_favicon'] = isset($input['site_favicon']) ? absint($input['site_favicon']) : 0;
$sanitized['enable_breadcrumbs'] = isset($input['enable_breadcrumbs']) ? (bool) $input['enable_breadcrumbs'] : false;
$sanitized['breadcrumb_separator'] = isset($input['breadcrumb_separator']) ? sanitize_text_field($input['breadcrumb_separator']) : '>';
$sanitized['date_format'] = isset($input['date_format']) ? sanitize_text_field($input['date_format']) : 'd/m/Y';
$sanitized['time_format'] = isset($input['time_format']) ? sanitize_text_field($input['time_format']) : 'H:i';
$sanitized['copyright_text'] = isset($input['copyright_text']) ? wp_kses_post($input['copyright_text']) : '';
// Social Media
$social_fields = array('facebook', 'twitter', 'instagram', 'linkedin', 'youtube');
foreach ($social_fields as $social) {
$key = 'social_' . $social;
$sanitized[$key] = isset($input[$key]) ? esc_url_raw($input[$key]) : '';
}
// Content Settings
$sanitized['excerpt_length'] = isset($input['excerpt_length']) ? absint($input['excerpt_length']) : 55;
$sanitized['excerpt_more'] = isset($input['excerpt_more']) ? sanitize_text_field($input['excerpt_more']) : '...';
$sanitized['default_post_layout'] = isset($input['default_post_layout']) ? sanitize_text_field($input['default_post_layout']) : 'right-sidebar';
$sanitized['default_page_layout'] = isset($input['default_page_layout']) ? sanitize_text_field($input['default_page_layout']) : 'right-sidebar';
$sanitized['archive_posts_per_page'] = isset($input['archive_posts_per_page']) ? absint($input['archive_posts_per_page']) : 10;
$sanitized['show_featured_image_single'] = isset($input['show_featured_image_single']) ? (bool) $input['show_featured_image_single'] : false;
$sanitized['show_author_box'] = isset($input['show_author_box']) ? (bool) $input['show_author_box'] : false;
$sanitized['enable_comments_posts'] = isset($input['enable_comments_posts']) ? (bool) $input['enable_comments_posts'] : false;
$sanitized['enable_comments_pages'] = isset($input['enable_comments_pages']) ? (bool) $input['enable_comments_pages'] : false;
$sanitized['show_post_meta'] = isset($input['show_post_meta']) ? (bool) $input['show_post_meta'] : false;
$sanitized['show_post_tags'] = isset($input['show_post_tags']) ? (bool) $input['show_post_tags'] : false;
$sanitized['show_post_categories'] = isset($input['show_post_categories']) ? (bool) $input['show_post_categories'] : false;
// Performance Settings
$sanitized['enable_lazy_loading'] = isset($input['enable_lazy_loading']) ? (bool) $input['enable_lazy_loading'] : false;
$sanitized['performance_remove_emoji'] = isset($input['performance_remove_emoji']) ? (bool) $input['performance_remove_emoji'] : false;
$sanitized['performance_remove_embeds'] = isset($input['performance_remove_embeds']) ? (bool) $input['performance_remove_embeds'] : false;
$sanitized['performance_remove_dashicons'] = isset($input['performance_remove_dashicons']) ? (bool) $input['performance_remove_dashicons'] : false;
$sanitized['performance_defer_js'] = isset($input['performance_defer_js']) ? (bool) $input['performance_defer_js'] : false;
$sanitized['performance_minify_html'] = isset($input['performance_minify_html']) ? (bool) $input['performance_minify_html'] : false;
$sanitized['performance_disable_gutenberg'] = isset($input['performance_disable_gutenberg']) ? (bool) $input['performance_disable_gutenberg'] : false;
// Related Posts
$sanitized['enable_related_posts'] = isset($input['enable_related_posts']) ? (bool) $input['enable_related_posts'] : false;
$sanitized['related_posts_count'] = isset($input['related_posts_count']) ? absint($input['related_posts_count']) : 3;
$sanitized['related_posts_taxonomy'] = isset($input['related_posts_taxonomy']) ? sanitize_text_field($input['related_posts_taxonomy']) : 'category';
$sanitized['related_posts_title'] = isset($input['related_posts_title']) ? sanitize_text_field($input['related_posts_title']) : __('Related Posts', 'roi-theme');
$sanitized['related_posts_columns'] = isset($input['related_posts_columns']) ? absint($input['related_posts_columns']) : 3;
// Social Share Buttons
$sanitized['roi_enable_share_buttons'] = isset($input['roi_enable_share_buttons']) ? sanitize_text_field($input['roi_enable_share_buttons']) : '1';
$sanitized['roi_share_text'] = isset($input['roi_share_text']) ? sanitize_text_field($input['roi_share_text']) : __('Compartir:', 'roi-theme');
// Advanced Settings
$sanitized['custom_css'] = isset($input['custom_css']) ? roi_sanitize_css($input['custom_css']) : '';
$sanitized['custom_js_header'] = isset($input['custom_js_header']) ? roi_sanitize_js($input['custom_js_header']) : '';
$sanitized['custom_js_footer'] = isset($input['custom_js_footer']) ? roi_sanitize_js($input['custom_js_footer']) : '';
return $sanitized;
}
/**
* NOTE: All sanitization functions have been moved to inc/sanitize-functions.php
* to avoid function redeclaration errors. This includes:
* - roi_sanitize_css()
* - roi_sanitize_js()
* - roi_sanitize_integer()
* - roi_sanitize_text()
* - roi_sanitize_url()
* - roi_sanitize_html()
* - roi_sanitize_checkbox()
* - roi_sanitize_select()
*/

View File

@@ -1,659 +0,0 @@
<?php
/**
* Theme Options Page Template
*
* @package ROI_Theme
* @since 1.0.0
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
// Get current options
$options = get_option('roi_theme_options', roi_get_default_options());
?>
<div class="wrap roi-theme-options">
<div class="roi-options-header">
<div class="roi-options-logo">
<h2><?php _e('ROI Theme', 'roi-theme'); ?></h2>
<span class="version"><?php echo 'v' . ROI_VERSION; ?></span>
</div>
<div class="roi-options-actions">
<button type="button" class="button button-secondary" id="roi-export-options">
<span class="dashicons dashicons-download"></span>
<?php _e('Export Options', 'roi-theme'); ?>
</button>
<button type="button" class="button button-secondary" id="roi-import-options">
<span class="dashicons dashicons-upload"></span>
<?php _e('Import Options', 'roi-theme'); ?>
</button>
<button type="button" class="button button-secondary" id="roi-reset-options">
<span class="dashicons dashicons-image-rotate"></span>
<?php _e('Reset to Defaults', 'roi-theme'); ?>
</button>
</div>
</div>
<form method="post" action="options.php" class="roi-options-form">
<?php
settings_fields('roi_theme_options_group');
?>
<div class="roi-options-container">
<!-- Tabs Navigation -->
<div class="roi-tabs-nav">
<ul>
<li class="active">
<a href="#general" data-tab="general">
<span class="dashicons dashicons-admin-settings"></span>
<?php _e('General', 'roi-theme'); ?>
</a>
</li>
<li>
<a href="#content" data-tab="content">
<span class="dashicons dashicons-edit-page"></span>
<?php _e('Content', 'roi-theme'); ?>
</a>
</li>
<li>
<a href="#performance" data-tab="performance">
<span class="dashicons dashicons-performance"></span>
<?php _e('Performance', 'roi-theme'); ?>
</a>
</li>
<li>
<a href="#related-posts" data-tab="related-posts">
<span class="dashicons dashicons-admin-links"></span>
<?php _e('Related Posts', 'roi-theme'); ?>
</a>
</li>
<li>
<a href="#advanced" data-tab="advanced">
<span class="dashicons dashicons-admin-tools"></span>
<?php _e('Advanced', 'roi-theme'); ?>
</a>
</li>
</ul>
</div>
<!-- Tabs Content -->
<div class="roi-tabs-content">
<!-- General Tab -->
<div id="general" class="roi-tab-pane active">
<h2><?php _e('General Settings', 'roi-theme'); ?></h2>
<p class="description"><?php _e('Configure general theme settings including logo, branding, and social media.', 'roi-theme'); ?></p>
<table class="form-table">
<!-- Site Logo -->
<tr>
<th scope="row">
<label for="site_logo"><?php _e('Site Logo', 'roi-theme'); ?></label>
</th>
<td>
<div class="roi-image-upload">
<input type="hidden" name="roi_theme_options[site_logo]" id="site_logo" value="<?php echo esc_attr($options['site_logo'] ?? 0); ?>" class="roi-image-id" />
<div class="roi-image-preview">
<?php
$logo_id = $options['site_logo'] ?? 0;
if ($logo_id) {
echo wp_get_attachment_image($logo_id, 'medium', false, array('class' => 'roi-preview-image'));
}
?>
</div>
<button type="button" class="button roi-upload-image"><?php _e('Upload Logo', 'roi-theme'); ?></button>
<button type="button" class="button roi-remove-image" <?php echo (!$logo_id ? 'style="display:none;"' : ''); ?>><?php _e('Remove Logo', 'roi-theme'); ?></button>
<p class="description"><?php _e('Upload your site logo. Recommended size: 200x60px', 'roi-theme'); ?></p>
</div>
</td>
</tr>
<!-- Site Favicon -->
<tr>
<th scope="row">
<label for="site_favicon"><?php _e('Site Favicon', 'roi-theme'); ?></label>
</th>
<td>
<div class="roi-image-upload">
<input type="hidden" name="roi_theme_options[site_favicon]" id="site_favicon" value="<?php echo esc_attr($options['site_favicon'] ?? 0); ?>" class="roi-image-id" />
<div class="roi-image-preview">
<?php
$favicon_id = $options['site_favicon'] ?? 0;
if ($favicon_id) {
echo wp_get_attachment_image($favicon_id, 'thumbnail', false, array('class' => 'roi-preview-image'));
}
?>
</div>
<button type="button" class="button roi-upload-image"><?php _e('Upload Favicon', 'roi-theme'); ?></button>
<button type="button" class="button roi-remove-image" <?php echo (!$favicon_id ? 'style="display:none;"' : ''); ?>><?php _e('Remove Favicon', 'roi-theme'); ?></button>
<p class="description"><?php _e('Upload your site favicon. Recommended size: 32x32px or 64x64px', 'roi-theme'); ?></p>
</div>
</td>
</tr>
<!-- Enable Breadcrumbs -->
<tr>
<th scope="row">
<label for="enable_breadcrumbs"><?php _e('Enable Breadcrumbs', 'roi-theme'); ?></label>
</th>
<td>
<label class="roi-switch">
<input type="checkbox" name="roi_theme_options[enable_breadcrumbs]" id="enable_breadcrumbs" value="1" <?php checked(isset($options['enable_breadcrumbs']) ? $options['enable_breadcrumbs'] : true, true); ?> />
<span class="roi-slider"></span>
</label>
<p class="description"><?php _e('Show breadcrumbs navigation on pages and posts', 'roi-theme'); ?></p>
</td>
</tr>
<!-- Breadcrumb Separator -->
<tr>
<th scope="row">
<label for="breadcrumb_separator"><?php _e('Breadcrumb Separator', 'roi-theme'); ?></label>
</th>
<td>
<input type="text" name="roi_theme_options[breadcrumb_separator]" id="breadcrumb_separator" value="<?php echo esc_attr($options['breadcrumb_separator'] ?? '>'); ?>" class="regular-text" />
<p class="description"><?php _e('Character or symbol to separate breadcrumb items (e.g., >, /, »)', 'roi-theme'); ?></p>
</td>
</tr>
<!-- Date Format -->
<tr>
<th scope="row">
<label for="date_format"><?php _e('Date Format', 'roi-theme'); ?></label>
</th>
<td>
<input type="text" name="roi_theme_options[date_format]" id="date_format" value="<?php echo esc_attr($options['date_format'] ?? 'd/m/Y'); ?>" class="regular-text" />
<p class="description"><?php _e('PHP date format (e.g., d/m/Y, m/d/Y, Y-m-d)', 'roi-theme'); ?></p>
</td>
</tr>
<!-- Time Format -->
<tr>
<th scope="row">
<label for="time_format"><?php _e('Time Format', 'roi-theme'); ?></label>
</th>
<td>
<input type="text" name="roi_theme_options[time_format]" id="time_format" value="<?php echo esc_attr($options['time_format'] ?? 'H:i'); ?>" class="regular-text" />
<p class="description"><?php _e('PHP time format (e.g., H:i, g:i A)', 'roi-theme'); ?></p>
</td>
</tr>
<!-- Copyright Text -->
<tr>
<th scope="row">
<label for="copyright_text"><?php _e('Copyright Text', 'roi-theme'); ?></label>
</th>
<td>
<textarea name="roi_theme_options[copyright_text]" id="copyright_text" rows="3" class="large-text"><?php echo esc_textarea($options['copyright_text'] ?? sprintf(__('&copy; %s %s. All rights reserved.', 'roi-theme'), date('Y'), get_bloginfo('name'))); ?></textarea>
<p class="description"><?php _e('Footer copyright text. HTML allowed.', 'roi-theme'); ?></p>
</td>
</tr>
</table>
<h3><?php _e('Social Media Links', 'roi-theme'); ?></h3>
<table class="form-table">
<!-- Facebook -->
<tr>
<th scope="row">
<label for="social_facebook"><?php _e('Facebook URL', 'roi-theme'); ?></label>
</th>
<td>
<input type="url" name="roi_theme_options[social_facebook]" id="social_facebook" value="<?php echo esc_url($options['social_facebook'] ?? ''); ?>" class="regular-text" placeholder="https://facebook.com/yourpage" />
</td>
</tr>
<!-- Twitter -->
<tr>
<th scope="row">
<label for="social_twitter"><?php _e('Twitter URL', 'roi-theme'); ?></label>
</th>
<td>
<input type="url" name="roi_theme_options[social_twitter]" id="social_twitter" value="<?php echo esc_url($options['social_twitter'] ?? ''); ?>" class="regular-text" placeholder="https://twitter.com/youraccount" />
</td>
</tr>
<!-- Instagram -->
<tr>
<th scope="row">
<label for="social_instagram"><?php _e('Instagram URL', 'roi-theme'); ?></label>
</th>
<td>
<input type="url" name="roi_theme_options[social_instagram]" id="social_instagram" value="<?php echo esc_url($options['social_instagram'] ?? ''); ?>" class="regular-text" placeholder="https://instagram.com/youraccount" />
</td>
</tr>
<!-- LinkedIn -->
<tr>
<th scope="row">
<label for="social_linkedin"><?php _e('LinkedIn URL', 'roi-theme'); ?></label>
</th>
<td>
<input type="url" name="roi_theme_options[social_linkedin]" id="social_linkedin" value="<?php echo esc_url($options['social_linkedin'] ?? ''); ?>" class="regular-text" placeholder="https://linkedin.com/company/yourcompany" />
</td>
</tr>
<!-- YouTube -->
<tr>
<th scope="row">
<label for="social_youtube"><?php _e('YouTube URL', 'roi-theme'); ?></label>
</th>
<td>
<input type="url" name="roi_theme_options[social_youtube]" id="social_youtube" value="<?php echo esc_url($options['social_youtube'] ?? ''); ?>" class="regular-text" placeholder="https://youtube.com/yourchannel" />
</td>
</tr>
</table>
</div>
<!-- Content Tab -->
<div id="content" class="roi-tab-pane">
<h2><?php _e('Content Settings', 'roi-theme'); ?></h2>
<p class="description"><?php _e('Configure content display settings for posts, pages, and archives.', 'roi-theme'); ?></p>
<table class="form-table">
<!-- Excerpt Length -->
<tr>
<th scope="row">
<label for="excerpt_length"><?php _e('Excerpt Length', 'roi-theme'); ?></label>
</th>
<td>
<input type="number" name="roi_theme_options[excerpt_length]" id="excerpt_length" value="<?php echo esc_attr($options['excerpt_length'] ?? 55); ?>" class="small-text" min="10" max="500" />
<p class="description"><?php _e('Number of words to show in excerpt', 'roi-theme'); ?></p>
</td>
</tr>
<!-- Excerpt More -->
<tr>
<th scope="row">
<label for="excerpt_more"><?php _e('Excerpt More Text', 'roi-theme'); ?></label>
</th>
<td>
<input type="text" name="roi_theme_options[excerpt_more]" id="excerpt_more" value="<?php echo esc_attr($options['excerpt_more'] ?? '...'); ?>" class="regular-text" />
<p class="description"><?php _e('Text to append at the end of excerpts', 'roi-theme'); ?></p>
</td>
</tr>
<!-- Default Post Layout -->
<tr>
<th scope="row">
<label for="default_post_layout"><?php _e('Default Post Layout', 'roi-theme'); ?></label>
</th>
<td>
<select name="roi_theme_options[default_post_layout]" id="default_post_layout">
<option value="right-sidebar" <?php selected($options['default_post_layout'] ?? 'right-sidebar', 'right-sidebar'); ?>><?php _e('Right Sidebar', 'roi-theme'); ?></option>
<option value="left-sidebar" <?php selected($options['default_post_layout'] ?? 'right-sidebar', 'left-sidebar'); ?>><?php _e('Left Sidebar', 'roi-theme'); ?></option>
<option value="no-sidebar" <?php selected($options['default_post_layout'] ?? 'right-sidebar', 'no-sidebar'); ?>><?php _e('No Sidebar (Full Width)', 'roi-theme'); ?></option>
</select>
<p class="description"><?php _e('Default layout for single posts', 'roi-theme'); ?></p>
</td>
</tr>
<!-- Default Page Layout -->
<tr>
<th scope="row">
<label for="default_page_layout"><?php _e('Default Page Layout', 'roi-theme'); ?></label>
</th>
<td>
<select name="roi_theme_options[default_page_layout]" id="default_page_layout">
<option value="right-sidebar" <?php selected($options['default_page_layout'] ?? 'right-sidebar', 'right-sidebar'); ?>><?php _e('Right Sidebar', 'roi-theme'); ?></option>
<option value="left-sidebar" <?php selected($options['default_page_layout'] ?? 'right-sidebar', 'left-sidebar'); ?>><?php _e('Left Sidebar', 'roi-theme'); ?></option>
<option value="no-sidebar" <?php selected($options['default_page_layout'] ?? 'right-sidebar', 'no-sidebar'); ?>><?php _e('No Sidebar (Full Width)', 'roi-theme'); ?></option>
</select>
<p class="description"><?php _e('Default layout for pages', 'roi-theme'); ?></p>
</td>
</tr>
<!-- Archive Posts Per Page -->
<tr>
<th scope="row">
<label for="archive_posts_per_page"><?php _e('Archive Posts Per Page', 'roi-theme'); ?></label>
</th>
<td>
<input type="number" name="roi_theme_options[archive_posts_per_page]" id="archive_posts_per_page" value="<?php echo esc_attr($options['archive_posts_per_page'] ?? 10); ?>" class="small-text" min="1" max="100" />
<p class="description"><?php _e('Number of posts to show on archive pages. Set to 0 to use WordPress default.', 'roi-theme'); ?></p>
</td>
</tr>
<!-- Show Featured Image on Single Posts -->
<tr>
<th scope="row">
<label for="show_featured_image_single"><?php _e('Show Featured Image', 'roi-theme'); ?></label>
</th>
<td>
<label class="roi-switch">
<input type="checkbox" name="roi_theme_options[show_featured_image_single]" id="show_featured_image_single" value="1" <?php checked(isset($options['show_featured_image_single']) ? $options['show_featured_image_single'] : true, true); ?> />
<span class="roi-slider"></span>
</label>
<p class="description"><?php _e('Display featured image at the top of single posts', 'roi-theme'); ?></p>
</td>
</tr>
<!-- Show Author Box -->
<tr>
<th scope="row">
<label for="show_author_box"><?php _e('Show Author Box', 'roi-theme'); ?></label>
</th>
<td>
<label class="roi-switch">
<input type="checkbox" name="roi_theme_options[show_author_box]" id="show_author_box" value="1" <?php checked(isset($options['show_author_box']) ? $options['show_author_box'] : true, true); ?> />
<span class="roi-slider"></span>
</label>
<p class="description"><?php _e('Display author information box on single posts', 'roi-theme'); ?></p>
</td>
</tr>
<!-- Enable Comments on Posts -->
<tr>
<th scope="row">
<label for="enable_comments_posts"><?php _e('Enable Comments on Posts', 'roi-theme'); ?></label>
</th>
<td>
<label class="roi-switch">
<input type="checkbox" name="roi_theme_options[enable_comments_posts]" id="enable_comments_posts" value="1" <?php checked(isset($options['enable_comments_posts']) ? $options['enable_comments_posts'] : true, true); ?> />
<span class="roi-slider"></span>
</label>
<p class="description"><?php _e('Allow comments on blog posts', 'roi-theme'); ?></p>
</td>
</tr>
<!-- Enable Comments on Pages -->
<tr>
<th scope="row">
<label for="enable_comments_pages"><?php _e('Enable Comments on Pages', 'roi-theme'); ?></label>
</th>
<td>
<label class="roi-switch">
<input type="checkbox" name="roi_theme_options[enable_comments_pages]" id="enable_comments_pages" value="1" <?php checked(isset($options['enable_comments_pages']) ? $options['enable_comments_pages'] : false, true); ?> />
<span class="roi-slider"></span>
</label>
<p class="description"><?php _e('Allow comments on pages', 'roi-theme'); ?></p>
</td>
</tr>
<!-- Show Post Meta -->
<tr>
<th scope="row">
<label for="show_post_meta"><?php _e('Show Post Meta', 'roi-theme'); ?></label>
</th>
<td>
<label class="roi-switch">
<input type="checkbox" name="roi_theme_options[show_post_meta]" id="show_post_meta" value="1" <?php checked(isset($options['show_post_meta']) ? $options['show_post_meta'] : true, true); ?> />
<span class="roi-slider"></span>
</label>
<p class="description"><?php _e('Display post meta information (date, author, etc.)', 'roi-theme'); ?></p>
</td>
</tr>
<!-- Show Post Tags -->
<tr>
<th scope="row">
<label for="show_post_tags"><?php _e('Show Post Tags', 'roi-theme'); ?></label>
</th>
<td>
<label class="roi-switch">
<input type="checkbox" name="roi_theme_options[show_post_tags]" id="show_post_tags" value="1" <?php checked(isset($options['show_post_tags']) ? $options['show_post_tags'] : true, true); ?> />
<span class="roi-slider"></span>
</label>
<p class="description"><?php _e('Display tags on single posts', 'roi-theme'); ?></p>
</td>
</tr>
<!-- Show Post Categories -->
<tr>
<th scope="row">
<label for="show_post_categories"><?php _e('Show Post Categories', 'roi-theme'); ?></label>
</th>
<td>
<label class="roi-switch">
<input type="checkbox" name="roi_theme_options[show_post_categories]" id="show_post_categories" value="1" <?php checked(isset($options['show_post_categories']) ? $options['show_post_categories'] : true, true); ?> />
<span class="roi-slider"></span>
</label>
<p class="description"><?php _e('Display categories on single posts', 'roi-theme'); ?></p>
</td>
</tr>
</table>
</div>
<!-- Performance Tab -->
<div id="performance" class="roi-tab-pane">
<h2><?php _e('Performance Settings', 'roi-theme'); ?></h2>
<p class="description"><?php _e('Optimize your site performance with these settings. Be careful when enabling these options.', 'roi-theme'); ?></p>
<table class="form-table">
<!-- Enable Lazy Loading -->
<tr>
<th scope="row">
<label for="enable_lazy_loading"><?php _e('Enable Lazy Loading', 'roi-theme'); ?></label>
</th>
<td>
<label class="roi-switch">
<input type="checkbox" name="roi_theme_options[enable_lazy_loading]" id="enable_lazy_loading" value="1" <?php checked(isset($options['enable_lazy_loading']) ? $options['enable_lazy_loading'] : true, true); ?> />
<span class="roi-slider"></span>
</label>
<p class="description"><?php _e('Enable lazy loading for images to improve page load times', 'roi-theme'); ?></p>
</td>
</tr>
<!-- Remove Emoji Scripts -->
<tr>
<th scope="row">
<label for="performance_remove_emoji"><?php _e('Remove Emoji Scripts', 'roi-theme'); ?></label>
</th>
<td>
<label class="roi-switch">
<input type="checkbox" name="roi_theme_options[performance_remove_emoji]" id="performance_remove_emoji" value="1" <?php checked(isset($options['performance_remove_emoji']) ? $options['performance_remove_emoji'] : true, true); ?> />
<span class="roi-slider"></span>
</label>
<p class="description"><?php _e('Remove WordPress emoji scripts and styles (reduces HTTP requests)', 'roi-theme'); ?></p>
</td>
</tr>
<!-- Remove Embeds -->
<tr>
<th scope="row">
<label for="performance_remove_embeds"><?php _e('Remove Embeds', 'roi-theme'); ?></label>
</th>
<td>
<label class="roi-switch">
<input type="checkbox" name="roi_theme_options[performance_remove_embeds]" id="performance_remove_embeds" value="1" <?php checked(isset($options['performance_remove_embeds']) ? $options['performance_remove_embeds'] : false, true); ?> />
<span class="roi-slider"></span>
</label>
<p class="description"><?php _e('Remove WordPress embed scripts if you don\'t use oEmbed', 'roi-theme'); ?></p>
</td>
</tr>
<!-- Remove Dashicons on Frontend -->
<tr>
<th scope="row">
<label for="performance_remove_dashicons"><?php _e('Remove Dashicons', 'roi-theme'); ?></label>
</th>
<td>
<label class="roi-switch">
<input type="checkbox" name="roi_theme_options[performance_remove_dashicons]" id="performance_remove_dashicons" value="1" <?php checked(isset($options['performance_remove_dashicons']) ? $options['performance_remove_dashicons'] : true, true); ?> />
<span class="roi-slider"></span>
</label>
<p class="description"><?php _e('Remove Dashicons from frontend for non-logged in users', 'roi-theme'); ?></p>
</td>
</tr>
<!-- Defer JavaScript -->
<tr>
<th scope="row">
<label for="performance_defer_js"><?php _e('Defer JavaScript', 'roi-theme'); ?></label>
</th>
<td>
<label class="roi-switch">
<input type="checkbox" name="roi_theme_options[performance_defer_js]" id="performance_defer_js" value="1" <?php checked(isset($options['performance_defer_js']) ? $options['performance_defer_js'] : false, true); ?> />
<span class="roi-slider"></span>
</label>
<p class="description"><?php _e('Add defer attribute to JavaScript files (may break some scripts)', 'roi-theme'); ?></p>
</td>
</tr>
<!-- Minify HTML -->
<tr>
<th scope="row">
<label for="performance_minify_html"><?php _e('Minify HTML', 'roi-theme'); ?></label>
</th>
<td>
<label class="roi-switch">
<input type="checkbox" name="roi_theme_options[performance_minify_html]" id="performance_minify_html" value="1" <?php checked(isset($options['performance_minify_html']) ? $options['performance_minify_html'] : false, true); ?> />
<span class="roi-slider"></span>
</label>
<p class="description"><?php _e('Minify HTML output to reduce page size', 'roi-theme'); ?></p>
</td>
</tr>
<!-- Disable Gutenberg -->
<tr>
<th scope="row">
<label for="performance_disable_gutenberg"><?php _e('Disable Gutenberg', 'roi-theme'); ?></label>
</th>
<td>
<label class="roi-switch">
<input type="checkbox" name="roi_theme_options[performance_disable_gutenberg]" id="performance_disable_gutenberg" value="1" <?php checked(isset($options['performance_disable_gutenberg']) ? $options['performance_disable_gutenberg'] : false, true); ?> />
<span class="roi-slider"></span>
</label>
<p class="description"><?php _e('Disable Gutenberg editor and revert to classic editor', 'roi-theme'); ?></p>
</td>
</tr>
</table>
</div>
<!-- Related Posts Tab -->
<div id="related-posts" class="roi-tab-pane">
<h2><?php _e('Related Posts Settings', 'roi-theme'); ?></h2>
<p class="description"><?php _e('Configure related posts display on single post pages.', 'roi-theme'); ?></p>
<table class="form-table">
<!-- Enable Related Posts -->
<tr>
<th scope="row">
<label for="enable_related_posts"><?php _e('Enable Related Posts', 'roi-theme'); ?></label>
</th>
<td>
<label class="roi-switch">
<input type="checkbox" name="roi_theme_options[enable_related_posts]" id="enable_related_posts" value="1" <?php checked(isset($options['enable_related_posts']) ? $options['enable_related_posts'] : true, true); ?> />
<span class="roi-slider"></span>
</label>
<p class="description"><?php _e('Show related posts at the end of single posts', 'roi-theme'); ?></p>
</td>
</tr>
<!-- Related Posts Count -->
<tr>
<th scope="row">
<label for="related_posts_count"><?php _e('Number of Related Posts', 'roi-theme'); ?></label>
</th>
<td>
<input type="number" name="roi_theme_options[related_posts_count]" id="related_posts_count" value="<?php echo esc_attr($options['related_posts_count'] ?? 3); ?>" class="small-text" min="1" max="12" />
<p class="description"><?php _e('How many related posts to display', 'roi-theme'); ?></p>
</td>
</tr>
<!-- Related Posts Taxonomy -->
<tr>
<th scope="row">
<label for="related_posts_taxonomy"><?php _e('Relate Posts By', 'roi-theme'); ?></label>
</th>
<td>
<select name="roi_theme_options[related_posts_taxonomy]" id="related_posts_taxonomy">
<option value="category" <?php selected($options['related_posts_taxonomy'] ?? 'category', 'category'); ?>><?php _e('Category', 'roi-theme'); ?></option>
<option value="tag" <?php selected($options['related_posts_taxonomy'] ?? 'category', 'tag'); ?>><?php _e('Tag', 'roi-theme'); ?></option>
<option value="both" <?php selected($options['related_posts_taxonomy'] ?? 'category', 'both'); ?>><?php _e('Category and Tag', 'roi-theme'); ?></option>
</select>
<p class="description"><?php _e('How to determine related posts', 'roi-theme'); ?></p>
</td>
</tr>
<!-- Related Posts Title -->
<tr>
<th scope="row">
<label for="related_posts_title"><?php _e('Related Posts Title', 'roi-theme'); ?></label>
</th>
<td>
<input type="text" name="roi_theme_options[related_posts_title]" id="related_posts_title" value="<?php echo esc_attr($options['related_posts_title'] ?? __('Related Posts', 'roi-theme')); ?>" class="regular-text" />
<p class="description"><?php _e('Title to display above related posts section', 'roi-theme'); ?></p>
</td>
</tr>
<!-- Related Posts Columns -->
<tr>
<th scope="row">
<label for="related_posts_columns"><?php _e('Columns', 'roi-theme'); ?></label>
</th>
<td>
<select name="roi_theme_options[related_posts_columns]" id="related_posts_columns">
<option value="2" <?php selected($options['related_posts_columns'] ?? 3, 2); ?>><?php _e('2 Columns', 'roi-theme'); ?></option>
<option value="3" <?php selected($options['related_posts_columns'] ?? 3, 3); ?>><?php _e('3 Columns', 'roi-theme'); ?></option>
<option value="4" <?php selected($options['related_posts_columns'] ?? 3, 4); ?>><?php _e('4 Columns', 'roi-theme'); ?></option>
</select>
<p class="description"><?php _e('Number of columns to display related posts', 'roi-theme'); ?></p>
</td>
</tr>
</table>
</div>
<!-- Advanced Tab -->
<div id="advanced" class="roi-tab-pane">
<h2><?php _e('Advanced Settings', 'roi-theme'); ?></h2>
<p class="description"><?php _e('Advanced customization options. Use with caution.', 'roi-theme'); ?></p>
<table class="form-table">
<!-- Custom CSS -->
<tr>
<th scope="row">
<label for="custom_css"><?php _e('Custom CSS', 'roi-theme'); ?></label>
</th>
<td>
<textarea name="roi_theme_options[custom_css]" id="custom_css" rows="10" class="large-text code"><?php echo esc_textarea($options['custom_css'] ?? ''); ?></textarea>
<p class="description"><?php _e('Add custom CSS code. This will be added to the &lt;head&gt; section.', 'roi-theme'); ?></p>
</td>
</tr>
<!-- Custom JS Header -->
<tr>
<th scope="row">
<label for="custom_js_header"><?php _e('Custom JavaScript (Header)', 'roi-theme'); ?></label>
</th>
<td>
<textarea name="roi_theme_options[custom_js_header]" id="custom_js_header" rows="10" class="large-text code"><?php echo esc_textarea($options['custom_js_header'] ?? ''); ?></textarea>
<p class="description"><?php _e('Add custom JavaScript code. This will be added to the &lt;head&gt; section. Do not include &lt;script&gt; tags.', 'roi-theme'); ?></p>
</td>
</tr>
<!-- Custom JS Footer -->
<tr>
<th scope="row">
<label for="custom_js_footer"><?php _e('Custom JavaScript (Footer)', 'roi-theme'); ?></label>
</th>
<td>
<textarea name="roi_theme_options[custom_js_footer]" id="custom_js_footer" rows="10" class="large-text code"><?php echo esc_textarea($options['custom_js_footer'] ?? ''); ?></textarea>
<p class="description"><?php _e('Add custom JavaScript code. This will be added before the closing &lt;/body&gt; tag. Do not include &lt;script&gt; tags.', 'roi-theme'); ?></p>
</td>
</tr>
</table>
</div>
</div>
</div>
<?php submit_button(__('Save All Settings', 'roi-theme'), 'primary large', 'submit', true); ?>
</form>
</div>
<!-- Import Modal -->
<div id="roi-import-modal" class="roi-modal" style="display:none;">
<div class="roi-modal-content">
<span class="roi-modal-close">&times;</span>
<h2><?php _e('Import Options', 'roi-theme'); ?></h2>
<p><?php _e('Paste your exported options JSON here:', 'roi-theme'); ?></p>
<textarea id="roi-import-data" rows="10" class="large-text code"></textarea>
<p>
<button type="button" class="button button-primary" id="roi-import-submit"><?php _e('Import', 'roi-theme'); ?></button>
<button type="button" class="button" id="roi-import-cancel"><?php _e('Cancel', 'roi-theme'); ?></button>
</p>
</div>
</div>

View File

@@ -1,272 +0,0 @@
<?php
/**
* Related Posts Configuration Options
*
* This file provides helper functions and documentation for configuring
* related posts functionality via WordPress options.
*
* @package ROI_Theme
* @since 1.0.0
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Get all related posts options with their current values
*
* @return array Array of options with their values
*/
function roi_get_related_posts_options() {
return array(
'enabled' => array(
'key' => 'roi_related_posts_enabled',
'value' => get_option('roi_related_posts_enabled', true),
'type' => 'boolean',
'default' => true,
'label' => __('Enable Related Posts', 'roi-theme'),
'description' => __('Show related posts section at the end of single posts', 'roi-theme'),
),
'title' => array(
'key' => 'roi_related_posts_title',
'value' => get_option('roi_related_posts_title', __('Related Posts', 'roi-theme')),
'type' => 'text',
'default' => __('Related Posts', 'roi-theme'),
'label' => __('Section Title', 'roi-theme'),
'description' => __('Title displayed above related posts', 'roi-theme'),
),
'count' => array(
'key' => 'roi_related_posts_count',
'value' => get_option('roi_related_posts_count', 3),
'type' => 'number',
'default' => 3,
'min' => 1,
'max' => 12,
'label' => __('Number of Posts', 'roi-theme'),
'description' => __('Maximum number of related posts to display', 'roi-theme'),
),
'columns' => array(
'key' => 'roi_related_posts_columns',
'value' => get_option('roi_related_posts_columns', 3),
'type' => 'select',
'default' => 3,
'options' => array(
1 => __('1 Column', 'roi-theme'),
2 => __('2 Columns', 'roi-theme'),
3 => __('3 Columns', 'roi-theme'),
4 => __('4 Columns', 'roi-theme'),
),
'label' => __('Grid Columns', 'roi-theme'),
'description' => __('Number of columns in the grid layout (responsive)', 'roi-theme'),
),
'show_excerpt' => array(
'key' => 'roi_related_posts_show_excerpt',
'value' => get_option('roi_related_posts_show_excerpt', true),
'type' => 'boolean',
'default' => true,
'label' => __('Show Excerpt', 'roi-theme'),
'description' => __('Display post excerpt in related posts cards', 'roi-theme'),
),
'excerpt_length' => array(
'key' => 'roi_related_posts_excerpt_length',
'value' => get_option('roi_related_posts_excerpt_length', 20),
'type' => 'number',
'default' => 20,
'min' => 5,
'max' => 100,
'label' => __('Excerpt Length', 'roi-theme'),
'description' => __('Number of words in the excerpt', 'roi-theme'),
),
'show_date' => array(
'key' => 'roi_related_posts_show_date',
'value' => get_option('roi_related_posts_show_date', true),
'type' => 'boolean',
'default' => true,
'label' => __('Show Date', 'roi-theme'),
'description' => __('Display publication date in related posts', 'roi-theme'),
),
'show_category' => array(
'key' => 'roi_related_posts_show_category',
'value' => get_option('roi_related_posts_show_category', true),
'type' => 'boolean',
'default' => true,
'label' => __('Show Category', 'roi-theme'),
'description' => __('Display category badge on related posts', 'roi-theme'),
),
'bg_colors' => array(
'key' => 'roi_related_posts_bg_colors',
'value' => get_option('roi_related_posts_bg_colors', array(
'#1a73e8', '#e91e63', '#4caf50', '#ff9800', '#9c27b0', '#00bcd4',
)),
'type' => 'color_array',
'default' => array(
'#1a73e8', // Blue
'#e91e63', // Pink
'#4caf50', // Green
'#ff9800', // Orange
'#9c27b0', // Purple
'#00bcd4', // Cyan
),
'label' => __('Background Colors', 'roi-theme'),
'description' => __('Colors used for posts without featured images', 'roi-theme'),
),
);
}
/**
* Update a related posts option
*
* @param string $option_key The option key (without 'roi_related_posts_' prefix)
* @param mixed $value The new value
* @return bool True if updated successfully
*/
function roi_update_related_posts_option($option_key, $value) {
$full_key = 'roi_related_posts_' . $option_key;
return update_option($full_key, $value);
}
/**
* Reset related posts options to defaults
*
* @return bool True if reset successfully
*/
function roi_reset_related_posts_options() {
$options = roi_get_related_posts_options();
$success = true;
foreach ($options as $option) {
if (!update_option($option['key'], $option['default'])) {
$success = false;
}
}
return $success;
}
/**
* Example: Programmatically configure related posts
*
* This function shows how to configure related posts options programmatically.
* You can call this from your functions.php or a plugin.
*
* @return void
*/
function roi_example_configure_related_posts() {
// Example usage - uncomment to use:
// Enable related posts
// update_option('roi_related_posts_enabled', true);
// Set custom title
// update_option('roi_related_posts_title', __('You Might Also Like', 'roi-theme'));
// Show 4 related posts
// update_option('roi_related_posts_count', 4);
// Use 2 columns layout
// update_option('roi_related_posts_columns', 2);
// Show excerpt with 30 words
// update_option('roi_related_posts_show_excerpt', true);
// update_option('roi_related_posts_excerpt_length', 30);
// Show date and category
// update_option('roi_related_posts_show_date', true);
// update_option('roi_related_posts_show_category', true);
// Custom background colors for posts without images
// update_option('roi_related_posts_bg_colors', array(
// '#FF6B6B', // Red
// '#4ECDC4', // Teal
// '#45B7D1', // Blue
// '#FFA07A', // Coral
// '#98D8C8', // Mint
// '#F7DC6F', // Yellow
// ));
}
/**
* Filter hook example: Modify related posts query
*
* This example shows how to customize the related posts query.
* Add this to your functions.php or child theme.
*/
function roi_example_modify_related_posts_query($args, $post_id) {
// Example: Order by date instead of random
// $args['orderby'] = 'date';
// $args['order'] = 'DESC';
// Example: Only show posts from the last 6 months
// $args['date_query'] = array(
// array(
// 'after' => '6 months ago',
// ),
// );
// Example: Exclude specific category
// $args['category__not_in'] = array(5); // Replace 5 with category ID
return $args;
}
// add_filter('roi_related_posts_args', 'roi_example_modify_related_posts_query', 10, 2);
/**
* Get documentation for related posts configuration
*
* @return array Documentation array
*/
function roi_get_related_posts_documentation() {
return array(
'overview' => array(
'title' => __('Related Posts Overview', 'roi-theme'),
'content' => __(
'The related posts feature automatically displays relevant posts at the end of each blog post. ' .
'Posts are related based on shared categories and displayed in a responsive Bootstrap grid.',
'roi-theme'
),
),
'features' => array(
'title' => __('Key Features', 'roi-theme'),
'items' => array(
__('Automatic category-based matching', 'roi-theme'),
__('Responsive Bootstrap 5 grid layout', 'roi-theme'),
__('Configurable number of posts and columns', 'roi-theme'),
__('Support for posts with and without featured images', 'roi-theme'),
__('Beautiful color backgrounds for posts without images', 'roi-theme'),
__('Customizable excerpt length', 'roi-theme'),
__('Optional display of dates and categories', 'roi-theme'),
__('Smooth hover animations', 'roi-theme'),
__('Print-friendly styles', 'roi-theme'),
__('Dark mode support', 'roi-theme'),
),
),
'configuration' => array(
'title' => __('How to Configure', 'roi-theme'),
'methods' => array(
'database' => array(
'title' => __('Via WordPress Options API', 'roi-theme'),
'code' => "update_option('roi_related_posts_enabled', true);\nupdate_option('roi_related_posts_count', 4);",
),
'filter' => array(
'title' => __('Via Filter Hook', 'roi-theme'),
'code' => "add_filter('roi_related_posts_args', function(\$args, \$post_id) {\n \$args['posts_per_page'] = 6;\n return \$args;\n}, 10, 2);",
),
),
),
'customization' => array(
'title' => __('Customization Examples', 'roi-theme'),
'examples' => array(
array(
'title' => __('Change title and layout', 'roi-theme'),
'code' => "update_option('roi_related_posts_title', 'También te puede interesar');\nupdate_option('roi_related_posts_columns', 4);",
),
array(
'title' => __('Customize colors', 'roi-theme'),
'code' => "update_option('roi_related_posts_bg_colors', array(\n '#FF6B6B',\n '#4ECDC4',\n '#45B7D1'\n));",
),
),
),
);
}

View File

@@ -1,217 +0,0 @@
<?php
/**
* Theme Options Admin Page
*
* @package ROI_Theme
* @since 1.0.0
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Add admin menu
* DESACTIVADO: Ahora se usa el nuevo Admin Panel en admin/includes/class-admin-menu.php
*/
/*
function roi_add_admin_menu() {
add_theme_page(
__('ROI Theme Options', 'roi-theme'), // Page title
__('Theme Options', 'roi-theme'), // Menu title
'manage_options', // Capability
'roi-theme-options', // Menu slug
'roi_render_options_page', // Callback function
30 // Position
);
}
add_action('admin_menu', 'roi_add_admin_menu');
*/
/**
* Render the options page
*/
function roi_render_options_page() {
// Check user capabilities
if (!current_user_can('manage_options')) {
wp_die(__('You do not have sufficient permissions to access this page.', 'roi-theme'));
}
// Load the template
include get_template_directory() . '/admin/theme-options/options-page-template.php';
}
/**
* Enqueue admin scripts and styles
*/
function roi_enqueue_admin_scripts($hook) {
// Only load on our theme options page
if ($hook !== 'appearance_page_roi-theme-options') {
return;
}
// Enqueue WordPress media uploader
wp_enqueue_media();
// Enqueue admin styles
wp_enqueue_style(
'roiadmin-options',
get_template_directory_uri() . '/admin/assets/css/theme-options.css',
array(),
ROI_VERSION
);
// Enqueue admin scripts
wp_enqueue_script(
'roiadmin-options',
get_template_directory_uri() . '/admin/assets/js/theme-options.js',
array('jquery', 'wp-color-picker'),
ROI_VERSION,
true
);
// Localize script
wp_localize_script('roiadmin-options', 'rroiminOptions', array(
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('roi_admin_nonce'),
'strings' => array(
'selectImage' => __('Select Image', 'roi-theme'),
'useImage' => __('Use Image', 'roi-theme'),
'removeImage' => __('Remove Image', 'roi-theme'),
'confirmReset' => __('Are you sure you want to reset all options to default values? This cannot be undone.', 'roi-theme'),
'saved' => __('Settings saved successfully!', 'roi-theme'),
'error' => __('An error occurred while saving settings.', 'roi-theme'),
),
));
}
add_action('admin_enqueue_scripts', 'roi_enqueue_admin_scripts');
/**
* Add settings link to theme actions
*/
function roi_add_settings_link($links) {
$settings_link = '<a href="' . admin_url('themes.php?page=roi-theme-options') . '">' . __('Settings', 'roi-theme') . '</a>';
array_unshift($links, $settings_link);
return $links;
}
add_filter('theme_action_links_' . get_template(), 'roi_add_settings_link');
/**
* AJAX handler for resetting options
*/
function roi_reset_options_ajax() {
check_ajax_referer('roi_admin_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(array('message' => __('Insufficient permissions.', 'roi-theme')));
}
// Delete options to reset to defaults
delete_option('roi_theme_options');
wp_send_json_success(array('message' => __('Options reset to defaults successfully.', 'roi-theme')));
}
add_action('wp_ajax_roi_reset_options', 'roi_reset_options_ajax');
/**
* AJAX handler for exporting options
*/
function roi_export_options_ajax() {
check_ajax_referer('roi_admin_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(array('message' => __('Insufficient permissions.', 'roi-theme')));
}
$options = get_option('roi_theme_options', array());
wp_send_json_success(array(
'data' => json_encode($options, JSON_PRETTY_PRINT),
'filename' => 'roi-theme-options-' . date('Y-m-d') . '.json'
));
}
add_action('wp_ajax_roi_export_options', 'roi_export_options_ajax');
/**
* AJAX handler for importing options
*/
function roi_import_options_ajax() {
check_ajax_referer('roi_admin_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(array('message' => __('Insufficient permissions.', 'roi-theme')));
}
if (!isset($_POST['import_data'])) {
wp_send_json_error(array('message' => __('No import data provided.', 'roi-theme')));
}
$import_data = json_decode(stripslashes($_POST['import_data']), true);
if (json_last_error() !== JSON_ERROR_NONE) {
wp_send_json_error(array('message' => __('Invalid JSON data.', 'roi-theme')));
}
// Sanitize imported data
$sanitized_data = roi_sanitize_options($import_data);
// Update options
update_option('roi_theme_options', $sanitized_data);
wp_send_json_success(array('message' => __('Options imported successfully.', 'roi-theme')));
}
add_action('wp_ajax_roi_import_options', 'roi_import_options_ajax');
/**
* Add admin notices
*/
function roi_admin_notices() {
$screen = get_current_screen();
if ($screen->id !== 'appearance_page_roi-theme-options') {
return;
}
// Check if settings were updated
if (isset($_GET['settings-updated']) && $_GET['settings-updated'] === 'true') {
?>
<div class="notice notice-success is-dismissible">
<p><?php _e('Settings saved successfully!', 'roi-theme'); ?></p>
</div>
<?php
}
}
add_action('admin_notices', 'roi_admin_notices');
/**
* Register theme options in Customizer as well (for preview)
*/
function roi_customize_register($wp_customize) {
// Add a panel for theme options
$wp_customize->add_panel('roi_theme_options', array(
'title' => __('ROI Theme Options', 'roi-theme'),
'description' => __('Configure theme options (Also available in Theme Options page)', 'roi-theme'),
'priority' => 10,
));
// General Section
$wp_customize->add_section('roi_general', array(
'title' => __('General Settings', 'roi-theme'),
'panel' => 'roi_theme_options',
'priority' => 10,
));
// Enable breadcrumbs
$wp_customize->add_setting('roi_theme_options[enable_breadcrumbs]', array(
'default' => true,
'type' => 'option',
'sanitize_callback' => 'roi_sanitize_checkbox',
));
$wp_customize->add_control('roi_theme_options[enable_breadcrumbs]', array(
'label' => __('Enable Breadcrumbs', 'roi-theme'),
'section' => 'roi_general',
'type' => 'checkbox',
));
}
add_action('customize_register', 'roi_customize_register');

View File

@@ -1,36 +0,0 @@
/**
* Buttons Styles
*
* RESPONSABILIDAD: Estilos de botones personalizados del tema
* - Botón Let's Talk (navbar)
* - Otros botones custom del tema
*
* REACTIVADO: Issue #121 - Arquitectura de separación de componentes
* El CSS NO debe estar en style.css sino en archivos individuales
*
* @package ROI_Theme
* @since 1.0.7
*/
/* ========================================
Botón Let's Talk (Navbar)
======================================== */
.btn-lets-talk {
background-color: var(--color-orange-primary) !important;
color: #ffffff !important;
font-weight: 600;
padding: 0.5rem 1.5rem;
border: none;
border-radius: 6px;
transition: all 0.3s ease;
}
.btn-lets-talk:hover {
background-color: var(--color-orange-hover) !important;
color: #ffffff !important;
}
.btn-lets-talk i {
color: #ffffff;
}

View File

@@ -1,54 +0,0 @@
/**
* CTA A/B Testing Styles
*
* CSS EXACTO copiado del template style.css (líneas 835-865)
* Sin extras, sin !important innecesario, sin media queries complicadas
*
* @package ROI_Theme
* @since 1.0.2
*/
.cta-section {
background: linear-gradient(135deg, var(--color-orange-primary) 0%, var(--color-orange-light) 100%);
box-shadow: 0 8px 24px rgba(255, 133, 0, 0.3);
border-radius: 12px;
padding: 2rem;
}
.cta-section h3 {
color: #ffffff !important;
}
.cta-section p {
color: rgba(255, 255, 255, 0.95) !important;
}
.cta-button {
background-color: var(--color-orange-primary);
color: #ffffff;
font-weight: 600;
padding: 0.75rem 2rem;
border: none;
border-radius: 8px;
transition: all 0.3s ease;
text-decoration: none;
display: inline-block;
}
.cta-button:hover {
background-color: var(--color-orange-hover);
color: #ffffff;
text-decoration: none;
}
/* Responsive Mobile */
@media (max-width: 768px) {
.cta-section {
padding: 1.5rem;
}
.cta-button {
width: 100%;
margin-top: 1rem;
}
}

View File

@@ -1,93 +0,0 @@
/**
* CTA Box Sidebar Styles
*
* Styles for the CTA box component that appears in the sidebar
* below the Table of Contents on single posts.
*
* @package ROI_Theme
* @since 1.0.0
*/
/* ========================================
CTA Box Container
======================================== */
.cta-box-sidebar {
background: var(--color-orange-primary);
border-radius: 8px;
padding: 24px;
text-align: center;
margin-top: 0;
margin-bottom: 15px;
height: 250px;
display: flex;
flex-direction: column;
justify-content: center;
box-shadow: 0 4px 12px rgba(255, 133, 0, 0.2);
}
/* ========================================
CTA Box Content
======================================== */
.cta-box-title {
color: #ffffff;
font-weight: 700;
font-size: 1.25rem;
margin-bottom: 1rem;
}
.cta-box-text {
color: rgba(255, 255, 255, 0.95);
font-size: 0.9rem;
margin-bottom: 1rem;
}
/* ========================================
CTA Button
======================================== */
.btn-cta-box {
background-color: #ffffff;
color: var(--color-orange-primary);
font-weight: 700;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 8px;
transition: all 0.3s ease;
font-size: 1rem;
}
.btn-cta-box:hover {
background-color: var(--color-navy-primary);
color: #ffffff;
}
/* ========================================
Icon Spacing
======================================== */
.btn-cta-box i {
vertical-align: middle;
}
/* ========================================
Responsive Design
======================================== */
/* Hide on tablets and mobile */
@media (max-width: 991px) {
.cta-box-sidebar {
display: none; /* Ocultar en móviles */
}
}
/* ========================================
Print Styles
======================================== */
@media print {
.cta-box-sidebar {
display: none;
}
}

View File

@@ -1,85 +0,0 @@
/**
* Footer Contact Form Styles
*
* Styles for the footer section including the contact form
* and contact information.
*
* @package ROI_Theme
* @since 1.0.0
*/
/* ========================================
Contact Form Styles
======================================== */
.form-control {
border: 2px solid var(--color-neutral-100);
border-radius: 6px;
padding: 0.625rem 1rem;
transition: all 0.3s ease;
}
.form-control:focus {
border-color: var(--color-orange-primary);
outline: none;
}
.btn-contact-submit {
background-color: var(--color-orange-primary);
color: #ffffff;
font-weight: 600;
padding: 0.75rem 2rem;
border: none;
border-radius: 6px;
transition: all 0.3s ease;
}
.btn-contact-submit:hover {
background-color: var(--color-orange-hover);
color: #ffffff;
}
.btn-submit-form {
background-color: var(--color-orange-primary);
color: #ffffff;
font-weight: 600;
padding: 0.75rem;
border: none;
border-radius: 6px;
transition: all 0.3s ease;
}
.btn-submit-form:hover {
background-color: var(--color-orange-hover);
color: #ffffff;
}
/* ========================================
Contact Info Styles
======================================== */
.contact-info i {
color: var(--color-orange-primary);
}
/* ========================================
Contact Section - Text Colors Override
======================================== */
/*
* Fix Issue #128: Textos demasiado oscuros en sección de contacto
* Template usa #495057 (gris medio) en lugar de #212529 (gris oscuro)
* Aplicar solo a sección bg-secondary (sección de contacto arriba del footer)
*/
section.bg-secondary h2,
section.bg-secondary h3,
section.bg-secondary p {
color: #495057 !important; /* Bootstrap --bs-gray-700 - Gris medio */
}
section.bg-secondary h6 {
color: #495057 !important; /* Bootstrap --bs-gray-700 - Gris medio */
}
/* NOTA: NO sobrescribir estilos Bootstrap h6 - Template usa defaults */

View File

@@ -1,50 +0,0 @@
/**
* Footer Principal Styles
*
* RESPONSABILIDAD: Estilos del footer principal del sitio
* - Background navy dark
* - Títulos y enlaces
* - Botón de newsletter
* - Hover states
*
* @package ROI_Theme
* @since 1.0.19
* @source roi-theme-template/css/style.css (líneas 987-1021)
* @reference CSS-ESPECIFICO.md
*/
/* ========================================
FOOTER
======================================== */
footer {
background-color: var(--color-navy-dark);
color: rgba(255, 255, 255, 0.8);
padding: 3rem 0;
}
footer h5 {
color: #ffffff;
font-weight: 600;
margin-bottom: 1rem;
}
footer a {
color: rgba(255, 255, 255, 0.8);
text-decoration: none;
transition: color 0.3s ease;
}
footer a:hover {
color: var(--color-orange-primary);
}
footer .btn-primary {
background-color: var(--color-orange-primary);
border-color: var(--color-orange-primary);
}
footer .btn-primary:hover {
background-color: var(--color-orange-hover);
border-color: var(--color-orange-hover);
}

View File

@@ -1,69 +0,0 @@
/**
* Hero Section Styles
*
* RESPONSABILIDAD: Estilos del componente Hero Section
* - Contenedor principal con gradiente navy
* - Título H1 del post
* - Category badges con efecto glassmorphism
*
* CORRECCIÓN Issue #121: Este archivo estaba vacío (solo comentarios)
* diciendo que los estilos estaban en style.css
*
* Ahora contiene el CSS correcto del template según:
* _planeacion/_desarrollo-tema-roi/theme-documentation/08-componente-hero-section/CSS-ESPECIFICO.md
*
* ELIMINADO: hero-section.css (duplicado con CSS incorrecto)
* - hero-section.css usaba clases .hero-section y .hero-category-badge
* - El HTML real usa .hero-title y .category-badge
* - Se consolidó todo en este archivo con las clases correctas
*
* @package ROI_Theme
* @since 1.0.9
* @source roi-theme-template/css/style.css líneas 186-222
*/
/* ========================================
Hero Section
======================================== */
.hero-title {
background: linear-gradient(135deg, var(--color-navy-primary) 0%, var(--color-navy-light) 100%);
box-shadow: 0 4px 16px rgba(30, 58, 95, 0.25);
padding: 3rem 0;
}
.hero-title h1 {
color: #ffffff !important;
font-weight: 700;
line-height: 1.4;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
margin-bottom: 0;
}
/* ========================================
Category Badges (Glassmorphism Effect)
======================================== */
.category-badge {
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
color: rgba(255, 255, 255, 0.95);
padding: 0.375rem 0.875rem;
border-radius: 20px;
font-size: 0.813rem;
font-weight: 500;
text-decoration: none;
display: inline-block;
transition: all 0.3s ease;
}
.category-badge:hover {
background: rgba(255, 133, 0, 0.2);
border-color: rgba(255, 133, 0, 0.4);
color: #ffffff;
}
.category-badge i {
color: var(--color-orange-light);
}

View File

@@ -1,419 +0,0 @@
/**
* Modal de Contacto - Estilos
*
* Estilos para el modal de contacto con webhook
* Compatible con Bootstrap 5.3.2
*
* @package ROI_Theme
* @since 1.0.0
*/
/* ==========================================================================
1. ESTRUCTURA DEL MODAL
========================================================================== */
.modal-content {
border-radius: 16px;
border: none;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
overflow: hidden;
}
.modal-header {
padding: 1.5rem 1.5rem 1rem 1.5rem;
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
}
.modal-title {
font-size: 1.5rem;
color: #2c3e50;
font-weight: 700;
}
.btn-close {
opacity: 0.6;
transition: opacity 0.3s ease;
}
.btn-close:hover {
opacity: 1;
}
.btn-close:focus {
box-shadow: 0 0 0 0.25rem rgba(255, 133, 0, 0.25);
outline: none;
}
.modal-body {
padding: 1rem 1.5rem 1.5rem 1.5rem;
}
/* ==========================================================================
2. FORMULARIO
========================================================================== */
.form-label {
font-weight: 600;
color: #495057;
margin-bottom: 0.5rem;
font-size: 0.95rem;
}
.form-label .text-danger {
font-weight: 700;
margin-left: 2px;
}
.form-control {
border-radius: 8px;
border: 1px solid #dee2e6;
padding: 0.65rem 1rem;
transition: all 0.3s ease;
font-size: 0.95rem;
}
.form-control:hover {
border-color: #adb5bd;
}
.form-control:focus {
border-color: #FF8600;
box-shadow: 0 0 0 0.2rem rgba(255, 133, 0, 0.15);
outline: none;
}
.form-control.is-invalid {
border-color: #dc3545;
padding-right: calc(1.5em + 0.75rem);
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right calc(0.375em + 0.1875rem) center;
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
}
.form-control.is-invalid:focus {
border-color: #dc3545;
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
}
.form-control.is-valid {
border-color: #28a745;
padding-right: calc(1.5em + 0.75rem);
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right calc(0.375em + 0.1875rem) center;
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
}
.form-control.is-valid:focus {
border-color: #28a745;
box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);
}
.invalid-feedback {
display: none;
width: 100%;
margin-top: 0.25rem;
font-size: 0.875em;
color: #dc3545;
}
.form-control.is-invalid ~ .invalid-feedback {
display: block;
}
textarea.form-control {
resize: vertical;
min-height: 80px;
}
.form-text {
display: block;
margin-top: 0.25rem;
font-size: 0.875em;
color: #6c757d;
}
/* ==========================================================================
3. BOTÓN DE ENVÍO
========================================================================== */
.btn-submit-form {
background: linear-gradient(135deg, #FF5722 0%, #FF6B35 100%);
color: #ffffff;
font-weight: 600;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(255, 87, 34, 0.3);
position: relative;
overflow: hidden;
}
.btn-submit-form::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
transition: left 0.5s ease;
}
.btn-submit-form:hover {
background: linear-gradient(135deg, #E64A19 0%, #FF5722 100%);
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(255, 87, 34, 0.4);
}
.btn-submit-form:hover::before {
left: 100%;
}
.btn-submit-form:active {
transform: translateY(0);
box-shadow: 0 2px 8px rgba(255, 87, 34, 0.3);
}
.btn-submit-form:focus {
outline: none;
box-shadow: 0 0 0 0.25rem rgba(255, 133, 0, 0.5), 0 4px 12px rgba(255, 87, 34, 0.3);
}
.btn-submit-form:disabled {
opacity: 0.7;
cursor: not-allowed;
transform: none;
pointer-events: none;
}
/* Spinner en botón */
.spinner-border-sm {
width: 1rem;
height: 1rem;
border-width: 0.15em;
}
/* ==========================================================================
4. MENSAJES DE FEEDBACK
========================================================================== */
#formMessage {
animation: slideDown 0.3s ease-out;
border-radius: 8px;
font-size: 0.9rem;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.alert {
padding: 0.75rem 1rem;
margin-bottom: 0;
border: none;
border-radius: 8px;
}
.alert-success {
background-color: #d4edda;
color: #155724;
border-left: 4px solid #28a745;
}
.alert-danger {
background-color: #f8d7da;
color: #721c24;
border-left: 4px solid #dc3545;
}
.alert-warning {
background-color: #fff3cd;
color: #856404;
border-left: 4px solid #ffc107;
}
.alert-info {
background-color: #d1ecf1;
color: #0c5460;
border-left: 4px solid #17a2b8;
}
/* ==========================================================================
5. ANIMACIONES DEL MODAL
========================================================================== */
.modal.fade .modal-dialog {
transition: transform 0.3s ease-out, opacity 0.3s ease-out;
transform: translate(0, -50px);
}
.modal.show .modal-dialog {
transform: none;
}
/* Backdrop personalizado */
.modal-backdrop.show {
opacity: 0.6;
}
/* ==========================================================================
6. RESPONSIVE
========================================================================== */
/* Tablets y dispositivos pequeños */
@media (max-width: 768px) {
.modal-dialog {
margin: 1rem;
}
.modal-header {
padding: 1rem;
}
.modal-body {
padding: 0.75rem 1rem 1rem 1rem;
}
.modal-title {
font-size: 1.25rem;
}
.form-control {
font-size: 16px; /* Previene zoom en iOS */
}
}
/* Móviles pequeños */
@media (max-width: 576px) {
.modal-dialog {
margin: 0.5rem;
max-width: calc(100% - 1rem);
}
.modal-content {
border-radius: 12px;
}
.modal-body {
padding: 0.5rem 0.75rem 0.75rem 0.75rem;
}
.btn-submit-form {
padding: 0.65rem 1.25rem;
font-size: 0.95rem;
}
}
/* ==========================================================================
7. ACCESIBILIDAD
========================================================================== */
/* Indicador de foco visible para navegación por teclado */
.modal-content *:focus-visible {
outline: 2px solid #FF8600;
outline-offset: 2px;
}
/* Mejora de contraste para lectores de pantalla */
.screen-reader-text {
clip: rect(1px, 1px, 1px, 1px);
clip-path: inset(50%);
height: 1px;
width: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
}
/* High contrast mode support */
@media (prefers-contrast: high) {
.form-control {
border-width: 2px;
}
.btn-submit-form {
border: 2px solid #000;
}
}
/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
.modal.fade .modal-dialog,
.btn-submit-form,
.form-control,
.btn-close {
transition: none;
}
.btn-submit-form::before {
display: none;
}
#formMessage {
animation: none;
}
}
/* ==========================================================================
8. DARK MODE (OPCIONAL)
========================================================================== */
@media (prefers-color-scheme: dark) {
.modal-content {
background-color: #2c3e50;
color: #ecf0f1;
}
.modal-header {
background: linear-gradient(135deg, #34495e 0%, #2c3e50 100%);
}
.modal-title {
color: #ecf0f1;
}
.form-label {
color: #bdc3c7;
}
.form-control {
background-color: #34495e;
border-color: #4a5f7f;
color: #ecf0f1;
}
.form-control:focus {
background-color: #34495e;
border-color: #FF8600;
}
.form-text {
color: #95a5a6;
}
.btn-close {
filter: invert(1);
}
}
/* ==========================================================================
9. PRINT STYLES
========================================================================== */
@media print {
.modal,
.modal-backdrop {
display: none !important;
}
}

View File

@@ -1,144 +0,0 @@
/**
* Navbar Styles
*
* RESPONSABILIDAD: Estilos del componente de navegación principal
* - Navbar sticky
* - Navbar brand
* - Nav links y efectos hover
* - Dropdown menu
* - Dropdown items
*
* @package ROI_Theme
* @since 1.0.7
* @source roi-theme-template/css/style.css
*/
/* ========================================
Navbar Principal
======================================== */
.navbar {
position: sticky;
top: 0;
z-index: 1030;
background-color: var(--color-navy-primary) !important;
box-shadow: 0 4px 12px rgba(30, 58, 95, 0.15);
padding: 0.75rem 0;
transition: all 0.3s ease;
}
.navbar.scrolled {
box-shadow: 0 6px 20px rgba(30, 58, 95, 0.25);
}
/* ========================================
Navbar Brand
======================================== */
.navbar-brand {
color: #ffffff !important;
font-weight: 700;
font-size: 1.5rem;
transition: color 0.3s ease;
}
.navbar-brand:hover {
color: var(--color-orange-primary) !important;
}
/* ========================================
Nav Links
======================================== */
.nav-link {
color: rgba(255, 255, 255, 0.9) !important;
font-weight: 500;
position: relative;
padding: 0.5rem 0.65rem !important;
transition: all 0.3s ease;
font-size: 0.9rem;
white-space: nowrap;
}
.nav-link::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%) scaleX(0);
width: 80%;
height: 2px;
background: var(--color-orange-primary);
transition: transform 0.3s ease;
}
.nav-link:hover {
color: var(--color-orange-primary) !important;
background-color: rgba(255, 133, 0, 0.1);
border-radius: 4px;
}
.nav-link:hover::after {
transform: translateX(-50%) scaleX(1);
}
/* ========================================
Dropdown Menu
======================================== */
.dropdown-menu {
background: #ffffff;
border: none;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
border-radius: 8px;
padding: 0.5rem 0;
max-height: 70vh;
overflow-y: auto;
scroll-behavior: smooth;
scrollbar-width: thin;
scrollbar-color: var(--color-gray-400) transparent;
}
/* Webkit browsers (Chrome, Safari, Edge) scrollbar */
.dropdown-menu::-webkit-scrollbar {
width: 6px;
}
.dropdown-menu::-webkit-scrollbar-track {
background: transparent;
}
.dropdown-menu::-webkit-scrollbar-thumb {
background-color: var(--color-gray-400);
border-radius: 3px;
}
.dropdown-menu::-webkit-scrollbar-thumb:hover {
background-color: var(--color-gray-500);
}
/* ========================================
Dropdown Items
======================================== */
.dropdown-item {
color: var(--color-neutral-600);
padding: 0.5rem 1.25rem;
transition: all 0.3s ease;
font-weight: 500;
}
.dropdown-item:hover {
background-color: rgba(255, 133, 0, 0.1);
color: var(--color-orange-primary);
}
/* ========================================
Dropdown Hover (Desktop Only)
======================================== */
@media (min-width: 992px) {
.nav-item:hover > .dropdown-menu {
display: block;
}
}

View File

@@ -1,134 +0,0 @@
/**
* Post Content Component
*
* Estilos para el contenedor y contenido de posts
* Source: roi-theme-template/css/style.css líneas 245-298
*
* @package ROI_Theme
* @since 1.0.0
*/
/* ============================================
POST CONTENT CONTAINER
============================================ */
.post-content {
background: #ffffff;
padding: 2rem;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
/* ============================================
POST CONTENT TYPOGRAPHY
============================================ */
.post-content h2 {
color: var(--color-navy-primary);
font-weight: 700;
margin-top: 2.5rem;
margin-bottom: 1.25rem;
padding-bottom: 0.75rem;
border-bottom: 3px solid var(--color-orange-primary);
}
.post-content h3 {
color: var(--color-navy-light);
font-weight: 600;
margin-top: 2rem;
margin-bottom: 1rem;
}
.post-content h4 {
font-size: 1.25rem;
font-weight: 600;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
color: #495057;
}
.post-content p {
color: var(--color-neutral-600);
line-height: 1.8;
margin-bottom: 1.25rem;
}
.post-content ul,
.post-content ol {
margin-bottom: 1.5rem;
padding-left: 2rem;
}
.post-content li {
margin-bottom: 0.5rem;
color: var(--color-neutral-600);
}
.post-content strong {
color: var(--color-navy-primary);
font-weight: 600;
}
.post-content a {
color: var(--color-orange-primary);
text-decoration: underline;
transition: color 0.3s ease;
}
.post-content a:hover {
color: var(--color-orange-hover);
}
.post-content blockquote {
border-left: 4px solid #0d6efd;
padding-left: 1.5rem;
margin: 2rem 0;
font-style: italic;
color: #6c757d;
}
.post-content code {
background: #f8f9fa;
padding: 0.2rem 0.4rem;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 0.95rem;
color: #e83e8c;
}
.post-content pre {
background: #f8f9fa;
padding: 1rem;
border-radius: 8px;
overflow-x: auto;
margin: 1.5rem 0;
}
.post-content pre code {
background: transparent;
padding: 0;
color: #212529;
}
/* ============================================
RESPONSIVE
============================================ */
@media (max-width: 767.98px) {
.post-content {
padding: 1.5rem;
}
.post-content h2 {
font-size: 1.5rem;
}
.post-content h3 {
font-size: 1.25rem;
}
.post-content p,
.post-content li {
font-size: 1rem;
}
}

View File

@@ -1,92 +0,0 @@
/**
* Related Posts Component
*
* RESPONSABILIDAD: Estilos para las cards de posts relacionados
* - Cards con borde izquierdo navy de 4px
* - Hover effect con borde naranja
* - Cursor pointer
* - Integración con Bootstrap 5 cards
*
* @package ROI_Theme
* @since 1.0.17
* @source roi-theme-template/css/style.css (líneas del template)
* @reference CSS-ESPECIFICO.md líneas 62-132
*/
/* ========================================
Related Posts Section
======================================== */
.related-posts {
margin: 3rem 0;
}
.related-posts h2 {
color: var(--color-navy-primary);
font-weight: 700;
margin-bottom: 2rem;
}
/* ========================================
Related Posts Cards
======================================== */
.related-posts .card {
cursor: pointer;
background: #ffffff !important;
border: 1px solid var(--color-neutral-100) !important;
border-left: 4px solid var(--color-neutral-600) !important;
transition: all 0.3s ease;
height: 100%;
}
.related-posts .card:hover {
background: var(--color-neutral-50) !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1) !important;
border-left-color: var(--color-orange-primary) !important;
}
.related-posts .card-body {
padding: 1.5rem !important;
}
.related-posts .card-title {
color: var(--color-navy-primary) !important;
font-weight: 600;
font-size: 0.95rem;
line-height: 1.4;
}
.related-posts a {
text-decoration: none;
}
.related-posts a:hover .card-title {
color: var(--color-orange-primary) !important;
}
/* ========================================
Pagination (si existe)
======================================== */
.pagination .page-link {
color: var(--color-neutral-600);
border: 1px solid var(--color-neutral-100);
padding: 0.5rem 1rem;
margin: 0 0.25rem;
border-radius: 4px;
font-weight: 500;
transition: all 0.3s ease;
}
.pagination .page-link:hover {
background-color: rgba(255, 133, 0, 0.1);
border-color: var(--color-orange-primary);
color: var(--color-orange-primary);
}
.pagination .page-item.active .page-link {
background-color: var(--color-orange-primary);
border-color: var(--color-orange-primary);
color: #ffffff;
}

View File

@@ -1,22 +0,0 @@
/**
* Social Share Buttons - Estilos Mínimos
*
* Según CSS-ESPECIFICO.md de la documentación:
* Solo 2 reglas CSS simples. Bootstrap maneja el resto.
*
* Fuente: roi-theme-template/css/style.css líneas 795-806
*
* @package ROI_Theme
* @since 1.0.0
*/
/* === SHARE BUTTONS === */
.share-buttons .btn {
transition: all 0.3s ease;
border-width: 2px;
}
.share-buttons .btn:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

View File

@@ -1,124 +0,0 @@
/**
* Sidebar TOC (Table of Contents) Styles
*
* RESPONSABILIDAD: Estilos del componente TOC Sidebar
* - Contenedor sticky (.sidebar-sticky)
* - Contenedor TOC (.toc-container)
* - Título del TOC (.toc-container h4)
* - Lista de enlaces (.toc-list)
* - Items y enlaces del TOC
* - Scrollbar personalizado
*
* @package ROI_Theme
* @since 1.0.5
* @source roi-theme-template/css/style.css líneas 663-746
*/
/* ========================================
Contenedor Sticky del Sidebar
======================================== */
.sidebar-sticky {
position: sticky;
top: 85px;
display: flex;
flex-direction: column;
}
/* ========================================
Contenedor del TOC
======================================== */
.toc-container {
margin-bottom: 13px;
background: #ffffff;
border: 1px solid var(--color-neutral-100);
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
padding: 12px 16px;
max-height: calc(100vh - 71px - 10px - 250px - 15px - 15px);
display: flex;
flex-direction: column;
}
/* ========================================
Título del TOC
======================================== */
.toc-container h4 {
color: var(--color-navy-primary);
padding-bottom: 8px;
border-bottom: 2px solid var(--color-neutral-100);
margin-bottom: 0.75rem;
font-weight: 600;
text-align: left;
font-size: 1rem;
font-style: normal;
}
/* ========================================
Lista de Enlaces del TOC
======================================== */
.toc-list {
overflow-y: auto;
padding-right: 0.5rem;
list-style: none;
flex: 1;
min-height: 0;
}
.toc-container li {
margin-bottom: 0.15rem;
}
/* ========================================
Enlaces del TOC
======================================== */
.toc-container a {
display: block;
padding: 0.3rem 0.85rem;
color: var(--color-neutral-600);
text-decoration: none;
border-left: 3px solid transparent;
transition: all 0.3s ease;
border-radius: 4px;
font-size: 0.9rem;
line-height: 1.3;
}
.toc-container a:hover {
background: var(--color-neutral-50);
border-left-color: var(--color-navy-primary);
color: var(--color-navy-primary);
}
.toc-container a.active {
background: var(--color-neutral-50);
border-left-color: var(--color-navy-primary);
color: var(--color-navy-primary);
font-weight: 600;
}
/* ========================================
Scrollbar Personalizado (Webkit)
======================================== */
.toc-list::-webkit-scrollbar {
width: 6px;
}
.toc-list::-webkit-scrollbar-track {
background: var(--color-neutral-50);
border-radius: 3px;
}
.toc-list::-webkit-scrollbar-thumb {
background: var(--color-neutral-600);
border-radius: 3px;
}
.toc-list::-webkit-scrollbar-thumb:hover {
background: var(--color-neutral-700);
}

View File

@@ -1,48 +0,0 @@
/**
* Top Notification Bar Styles
*
* RESPONSABILIDAD: Estilos de la barra de notificación superior
* - Contenedor principal
* - Palabras destacadas (strong)
* - Iconos
* - Enlaces
*
* CORRECCIÓN Issue #121: Este archivo contenía CSS avanzado (position fixed, botón cerrar)
* que NO coincidía con el HTML simple del template en header.php
*
* Ahora contiene el CSS correcto del template según:
* _planeacion/_desarrollo-tema-roi/theme-documentation/05-componente-top-bar/CSS-ESPECIFICO.md
*
* @package ROI_Theme
* @since 1.0.8
* @source roi-theme-template/css/style.css líneas 57-80
*/
/* ========================================
Top Notification Bar
======================================== */
.top-notification-bar {
background-color: var(--color-navy-dark);
color: #ffffff;
padding: 0.5rem 0;
font-size: 0.9rem;
text-align: center;
}
.top-notification-bar strong {
color: var(--color-orange-primary);
}
.top-notification-bar i {
color: var(--color-orange-primary);
}
.top-notification-bar a {
color: #ffffff;
transition: color 0.3s ease;
}
.top-notification-bar a:hover {
color: var(--color-orange-primary);
}

View File

@@ -37,9 +37,7 @@
color: var(--color-orange-primary); color: var(--color-orange-primary);
background-color: rgba(255, 133, 0, 0.1); background-color: rgba(255, 133, 0, 0.1);
border-color: var(--color-orange-primary); border-color: var(--color-orange-primary);
transform: translateY(-2px); text-decoration: none;
box-shadow: 0 4px 8px rgba(255, 133, 0, 0.15);
z-index: 2;
} }
.page-link:focus { .page-link:focus {
@@ -53,17 +51,8 @@
/* Active page */ /* Active page */
.page-item.active .page-link { .page-item.active .page-link {
color: #ffffff; color: #ffffff;
background: var(--color-orange-primary); background-color: var(--color-orange-primary);
border-color: var(--color-orange-primary); border-color: var(--color-orange-primary);
font-weight: 600;
box-shadow: 0 4px 12px rgba(255, 133, 0, 0.3);
z-index: 3;
}
.page-item.active .page-link:hover {
background: var(--color-orange-light);
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(255, 133, 0, 0.4);
} }
/* Disabled state */ /* Disabled state */

View File

@@ -2,93 +2,19 @@
/** /**
* Footer Template * Footer Template
* *
* Replica EXACTAMENTE la estructura del template (líneas 1093-1149) * Renderiza el footer usando el componente dinámico desde BD.
* Footer con 3 columnas de navegación + newsletter simple (solo email). * Los menús se gestionan desde Apariencia > Menús.
* La configuración se gestiona desde ROI Theme > Footer.
* *
* @package ROI_Theme * @package ROI_Theme
* @since 1.0.0 * @since 1.0.0
*/ */
// Renderizar footer dinámico
echo roi_render_component('footer');
wp_footer();
?> ?>
<!-- Footer (Template líneas 1093-1149) -->
<footer class="py-5 mt-0 bg-dark text-white">
<div class="container">
<div class="row">
<!-- Sección 1: Navegación -->
<div class="col-6 col-md-2 mb-3">
<h5><?php esc_html_e('Recursos', 'roi-theme'); ?></h5>
<ul class="nav flex-column">
<li class="nav-item mb-2"><a href="<?php echo home_url('/'); ?>" class="nav-link p-0 text-white"><?php esc_html_e('Inicio', 'roi-theme'); ?></a></li>
<li class="nav-item mb-2"><a href="<?php echo home_url('/blog'); ?>" class="nav-link p-0 text-white"><?php esc_html_e('Blog', 'roi-theme'); ?></a></li>
<li class="nav-item mb-2"><a href="<?php echo home_url('/catalogo'); ?>" class="nav-link p-0 text-white"><?php esc_html_e('Catálogo', 'roi-theme'); ?></a></li>
<li class="nav-item mb-2"><a href="<?php echo home_url('/precios'); ?>" class="nav-link p-0 text-white"><?php esc_html_e('Precios', 'roi-theme'); ?></a></li>
<li class="nav-item mb-2"><a href="<?php echo home_url('/nosotros'); ?>" class="nav-link p-0 text-white"><?php esc_html_e('Nosotros', 'roi-theme'); ?></a></li>
</ul>
</div>
<!-- Sección 2: Navegación -->
<div class="col-6 col-md-2 mb-3">
<h5><?php esc_html_e('Soporte', 'roi-theme'); ?></h5>
<ul class="nav flex-column">
<li class="nav-item mb-2"><a href="<?php echo home_url('/faq'); ?>" class="nav-link p-0 text-white"><?php esc_html_e('Preguntas Frecuentes', 'roi-theme'); ?></a></li>
<li class="nav-item mb-2"><a href="<?php echo home_url('/ayuda'); ?>" class="nav-link p-0 text-white"><?php esc_html_e('Centro de Ayuda', 'roi-theme'); ?></a></li>
<li class="nav-item mb-2"><a href="<?php echo home_url('/contacto'); ?>" class="nav-link p-0 text-white"><?php esc_html_e('Contacto', 'roi-theme'); ?></a></li>
<li class="nav-item mb-2"><a href="<?php echo home_url('/politicas'); ?>" class="nav-link p-0 text-white"><?php esc_html_e('Políticas', 'roi-theme'); ?></a></li>
<li class="nav-item mb-2"><a href="<?php echo home_url('/terminos'); ?>" class="nav-link p-0 text-white"><?php esc_html_e('Términos', 'roi-theme'); ?></a></li>
</ul>
</div>
<!-- Sección 3: Navegación -->
<div class="col-6 col-md-2 mb-3">
<h5><?php esc_html_e('Empresa', 'roi-theme'); ?></h5>
<ul class="nav flex-column">
<li class="nav-item mb-2"><a href="<?php echo home_url('/nosotros'); ?>" class="nav-link p-0 text-white"><?php esc_html_e('Acerca de', 'roi-theme'); ?></a></li>
<li class="nav-item mb-2"><a href="<?php echo home_url('/equipo'); ?>" class="nav-link p-0 text-white"><?php esc_html_e('Equipo', 'roi-theme'); ?></a></li>
<li class="nav-item mb-2"><a href="<?php echo home_url('/trabajos'); ?>" class="nav-link p-0 text-white"><?php esc_html_e('Trabaja con Nosotros', 'roi-theme'); ?></a></li>
<li class="nav-item mb-2"><a href="<?php echo home_url('/prensa'); ?>" class="nav-link p-0 text-white"><?php esc_html_e('Prensa', 'roi-theme'); ?></a></li>
<li class="nav-item mb-2"><a href="<?php echo home_url('/partners'); ?>" class="nav-link p-0 text-white"><?php esc_html_e('Partners', 'roi-theme'); ?></a></li>
</ul>
</div>
<!-- Newsletter Simple (solo email) -->
<div class="col-md-5 offset-md-1 mb-3">
<form>
<h5><?php esc_html_e('Suscríbete al Newsletter', 'roi-theme'); ?></h5>
<p><?php esc_html_e('Recibe las últimas actualizaciones de APUs.', 'roi-theme'); ?></p>
<div class="d-flex flex-column flex-sm-row w-100 gap-2">
<label for="newsletter1" class="visually-hidden"><?php esc_html_e('Email', 'roi-theme'); ?></label>
<input id="newsletter1" type="email" class="form-control" placeholder="<?php esc_attr_e('Email', 'roi-theme'); ?>">
<button class="btn btn-primary" type="button"><?php esc_html_e('Suscribirse', 'roi-theme'); ?></button>
</div>
</form>
</div>
</div>
<!-- Copyright y Redes Sociales -->
<div class="d-flex flex-column flex-sm-row justify-content-between py-4 my-4 border-top">
<p>&copy; <?php echo date('Y'); ?> <?php bloginfo('name'); ?>. <?php esc_html_e('Todos los derechos reservados.', 'roi-theme'); ?></p>
<ul class="list-unstyled d-flex">
<li class="ms-3">
<a class="link-light" href="https://twitter.com/tuusuario" target="_blank" rel="noopener" aria-label="<?php esc_attr_e('Twitter', 'roi-theme'); ?>">
<i class="bi bi-twitter"></i>
</a>
</li>
<li class="ms-3">
<a class="link-light" href="https://instagram.com/tuusuario" target="_blank" rel="noopener" aria-label="<?php esc_attr_e('Instagram', 'roi-theme'); ?>">
<i class="bi bi-instagram"></i>
</a>
</li>
<li class="ms-3">
<a class="link-light" href="https://facebook.com/tuusuario" target="_blank" rel="noopener" aria-label="<?php esc_attr_e('Facebook', 'roi-theme'); ?>">
<i class="bi bi-facebook"></i>
</a>
</li>
</ul>
</div>
</div>
</footer>
<?php wp_footer(); ?>
</body> </body>
</html> </html>

232
functions-addon.php Normal file
View File

@@ -0,0 +1,232 @@
<?php
// =============================================================================
// AUTOLOADER PARA COMPONENTES
// =============================================================================
spl_autoload_register(function ($class) {
// Mapeo de namespaces a directorios
$prefixes = [
'ROITheme\\Shared\\' => get_template_directory() . '/Shared/',
'ROITheme\\Public\\' => get_template_directory() . '/Public/',
'ROITheme\\Admin\\' => get_template_directory() . '/Admin/',
];
foreach ($prefixes as $prefix => $base_dir) {
$len = strlen($prefix);
if (strncmp($prefix, $class, $len) === 0) {
$relative_class = substr($class, $len);
$file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';
if (file_exists($file)) {
require $file;
return;
}
}
}
});
// =============================================================================
// HELPER FUNCTION: roi_get_navbar_setting()
// =============================================================================
/**
* Obtiene un valor de configuración del navbar desde la BD
*
* @param string $group Nombre del grupo (ej: 'media', 'visibility')
* @param string $attribute Nombre del atributo (ej: 'show_brand', 'logo_url')
* @param mixed $default Valor por defecto si no existe
* @return mixed Valor del atributo
*/
function roi_get_navbar_setting(string $group, string $attribute, $default = null) {
global $wpdb;
$table = $wpdb->prefix . 'roi_theme_component_settings';
$value = $wpdb->get_var($wpdb->prepare(
"SELECT attribute_value FROM {$table}
WHERE component_name = 'navbar'
AND group_name = %s
AND attribute_name = %s",
$group,
$attribute
));
if ($value === null) {
return $default;
}
// Convertir booleanos
if ($value === '1') return true;
if ($value === '0') return false;
// Intentar decodificar JSON
$decoded = json_decode($value, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
return $decoded;
}
return $value;
}
// =============================================================================
// HELPER FUNCTION: roi_render_component()
// =============================================================================
/**
* Renderiza un componente por su nombre
*
* @param string $componentName Nombre del componente
* @return string HTML del componente renderizado
*/
function roi_render_component(string $componentName): string {
global $wpdb;
// DEBUG: Trace component rendering
error_log("ROI Theme DEBUG: roi_render_component called with: {$componentName}");
try {
// Obtener datos del componente desde BD normalizada
$table = $wpdb->prefix . 'roi_theme_component_settings';
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT group_name, attribute_name, attribute_value
FROM {$table}
WHERE component_name = %s
ORDER BY group_name, attribute_name",
$componentName
));
if (empty($rows)) {
return '';
}
// Reconstruir estructura de datos agrupada
$data = [];
foreach ($rows as $row) {
if (!isset($data[$row->group_name])) {
$data[$row->group_name] = [];
}
// Decodificar valor
$value = $row->attribute_value;
// Convertir booleanos almacenados como '1' o '0'
if ($value === '1' || $value === '0') {
$value = ($value === '1');
} else {
// Intentar decodificar JSON
$decoded = json_decode($value, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
$value = $decoded;
}
}
$data[$row->group_name][$row->attribute_name] = $value;
}
// 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
// Crear instancia del componente
$component = new \ROITheme\Shared\Domain\Entities\Component(
$name,
$configuration,
$visibility
);
// Obtener renderer específico para el componente
$renderer = null;
// Crear instancia del CSSGeneratorService (reutilizable para todos los renderers que lo necesiten)
$cssGenerator = new \ROITheme\Shared\Infrastructure\Services\CSSGeneratorService();
switch ($componentName) {
// Componentes nuevos (namespace PascalCase correcto)
case 'top-notification-bar':
$renderer = new \ROITheme\Public\TopNotificationBar\Infrastructure\Ui\TopNotificationBarRenderer($cssGenerator);
break;
case 'navbar':
$renderer = new \ROITheme\Public\Navbar\Infrastructure\Ui\NavbarRenderer($cssGenerator);
break;
case 'cta-lets-talk':
$renderer = new \ROITheme\Public\CtaLetsTalk\Infrastructure\Ui\CtaLetsTalkRenderer($cssGenerator);
break;
case 'hero':
error_log("ROI Theme DEBUG: Creating HeroRenderer");
$renderer = new \ROITheme\Public\Hero\Infrastructure\Ui\HeroRenderer($cssGenerator);
error_log("ROI Theme DEBUG: HeroRenderer created successfully");
break;
// Componentes legacy (namespace minúsculas - pendiente migración)
case 'hero-section':
$renderer = new \ROITheme\Public\herosection\infrastructure\ui\HeroSectionRenderer();
break;
case 'featured-image':
$renderer = new \ROITheme\Public\FeaturedImage\Infrastructure\Ui\FeaturedImageRenderer($cssGenerator);
break;
case 'table-of-contents':
$renderer = new \ROITheme\Public\TableOfContents\Infrastructure\Ui\TableOfContentsRenderer($cssGenerator);
break;
case 'cta-box-sidebar':
$renderer = new \ROITheme\Public\CtaBoxSidebar\Infrastructure\Ui\CtaBoxSidebarRenderer($cssGenerator);
break;
case 'social-share':
$renderer = new \ROITheme\Public\SocialShare\Infrastructure\Ui\SocialShareRenderer($cssGenerator);
break;
case 'cta-post':
$renderer = new \ROITheme\Public\CtaPost\Infrastructure\Ui\CtaPostRenderer($cssGenerator);
break;
case 'related-post':
$renderer = new \ROITheme\Public\RelatedPost\Infrastructure\Ui\RelatedPostRenderer($cssGenerator);
break;
case 'contact-form':
$renderer = new \ROITheme\Public\ContactForm\Infrastructure\Ui\ContactFormRenderer($cssGenerator);
break;
case 'footer':
$renderer = new \ROITheme\Public\Footer\Infrastructure\Ui\FooterRenderer($cssGenerator);
break;
}
if (!$renderer) {
error_log("ROI Theme DEBUG: No renderer for {$componentName}");
return '';
}
error_log("ROI Theme DEBUG: Calling render() for {$componentName}");
$output = $renderer->render($component);
error_log("ROI Theme DEBUG: render() returned " . strlen($output) . " chars for {$componentName}");
return $output;
} catch (\Exception $e) {
// Always log errors for debugging
error_log('ROI Theme ERROR: Exception rendering component ' . $componentName . ': ' . $e->getMessage());
error_log('ROI Theme ERROR: Stack trace: ' . $e->getTraceAsString());
return '';
}
}
// =============================================================================
// CARGAR ARCHIVOS DE INC/
// =============================================================================
// CTA A/B Testing System
$cta_ab_testing = get_template_directory() . '/Inc/cta-ab-testing.php';
if (file_exists($cta_ab_testing)) {
require_once $cta_ab_testing;
}
// CTA Customizer Settings
$cta_customizer = get_template_directory() . '/Inc/customizer-cta.php';
if (file_exists($cta_customizer)) {
require_once $cta_customizer;
}
// =============================================================================
// ESTILOS BASE PARA TOP NOTIFICATION BAR
// =============================================================================
// =============================================================================
// NOTA: Los estilos de TOC y CTA Box Sidebar se generan dinámicamente
// desde la base de datos a través de sus respectivos Renderers.
// NO hardcodear CSS aquí - viola la arquitectura Clean Architecture.
// =============================================================================

View File

@@ -1,66 +1,240 @@
<?php <?php
declare(strict_types=1);
/** /**
* ROI Theme Functions and Definitions * ROI Theme - Clean Architecture Bootstrap
* *
* @package ROI_Theme * Este archivo es el punto de entrada del tema.
* @since 1.0.0 * Solo contiene bootstrap (carga del autoloader e inicialización del DI Container).
* TODO el código de lógica de negocio está en admin/, public/, shared/
*
* @package ROITheme
* @version 1.0.0
*/ */
// Exit if accessed directly // Prevenir acceso directo
if (!defined('ABSPATH')) { if (!defined('ABSPATH')) {
exit; exit;
} }
/** // Definir constante de versión del tema
* ======================================================================== define('ROI_VERSION', '1.0.19');
* BOOTSTRAP CLEAN ARCHITECTURE (Fase 1)
* ========================================================================
*
* Carga el autoloader de Composer y el DI Container.
* Esta sección inicializa la arquitectura limpia del tema.
*/
// Load Composer autoloader // =============================================================================
if (file_exists(__DIR__ . '/vendor/autoload.php')) { // 1. CARGAR AUTOLOADER MANUAL
require_once __DIR__ . '/vendor/autoload.php'; // =============================================================================
// IMPORTANTE: Cargar functions-addon.php PRIMERO para registrar el autoloader
// que permite cargar clases de ROITheme automáticamente
require_once __DIR__ . '/functions-addon.php';
// =============================================================================
// 2. CARGAR ARCHIVOS DE INC/ (ANTES DE LA CONFIGURACIÓN)
// =============================================================================
// IMPORTANTE: Cargar archivos inc/ ANTES del DI Container
// para que los estilos y scripts se registren correctamente
require_once get_template_directory() . '/Inc/sanitize-functions.php';
require_once get_template_directory() . '/Inc/theme-options-helpers.php';
require_once get_template_directory() . '/Inc/nav-walker.php';
require_once get_template_directory() . '/Inc/enqueue-scripts.php';
require_once get_template_directory() . '/Inc/customizer-fonts.php';
require_once get_template_directory() . '/Inc/seo.php';
require_once get_template_directory() . '/Inc/performance.php';
require_once get_template_directory() . '/Inc/critical-css.php';
require_once get_template_directory() . '/Inc/image-optimization.php';
require_once get_template_directory() . '/Inc/template-functions.php';
require_once get_template_directory() . '/Inc/template-tags.php';
require_once get_template_directory() . '/Inc/featured-image.php';
require_once get_template_directory() . '/Inc/category-badge.php';
require_once get_template_directory() . '/Inc/adsense-delay.php';
require_once get_template_directory() . '/Inc/related-posts.php';
require_once get_template_directory() . '/Inc/toc.php';
require_once get_template_directory() . '/Inc/apu-tables.php';
require_once get_template_directory() . '/Inc/search-disable.php';
require_once get_template_directory() . '/Inc/comments-disable.php';
require_once get_template_directory() . '/Inc/social-share.php';
require_once get_template_directory() . '/Inc/cta-ab-testing.php';
require_once get_template_directory() . '/Inc/customizer-cta.php';
// =============================================================================
// 3. INICIALIZAR DI CONTAINER (Clean Architecture)
// =============================================================================
use ROITheme\Shared\Infrastructure\DI\DIContainer;
try {
global $wpdb;
// Path a los schemas
$schemas_path = get_template_directory() . '/schemas';
// Crear instancia del DI Container con dependencias
$container = new DIContainer($wpdb, $schemas_path);
// TODO: Inicializar controladores cuando estén implementados (Fase 5+)
// $ajaxController = $container->getAjaxController();
} catch (\Throwable $e) {
// Manejar errores de inicialización (no crítico, solo log)
if (defined('WP_DEBUG') && WP_DEBUG) {
error_log('ROI Theme: Failed to initialize DI Container: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
}
// NO hacer return - permitir que el tema continúe funcionando
$container = null;
} }
// Initialize DI Container // =============================================================================
use ROITheme\Infrastructure\DI\DIContainer; // 3.1. INICIALIZAR PANEL DE ADMINISTRACIÓN (Clean Architecture)
// =============================================================================
/** use ROITheme\Admin\Domain\ValueObjects\MenuItem;
* Get DI Container instance use ROITheme\Admin\Application\UseCases\RenderDashboardUseCase;
* use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
* Helper function to access the DI Container throughout the theme. use ROITheme\Admin\Infrastructure\API\WordPress\AdminMenuRegistrar;
* use ROITheme\Admin\Infrastructure\Services\AdminAssetEnqueuer;
* @return DIContainer
*/ try {
function roi_container(): DIContainer { // Obtener Use Case para cargar configuraciones
return DIContainer::getInstance(); $getComponentSettingsUseCase = $container?->getGetComponentSettingsUseCase();
// Crear MenuItem con configuración del panel
$menuItem = new MenuItem(
pageTitle: 'ROI Theme - Panel de Administración',
menuTitle: 'ROI Theme',
capability: 'manage_options',
menuSlug: 'roi-theme-admin',
icon: 'dashicons-admin-settings',
position: 60
);
// Crear renderer del dashboard con inyección del Use Case
$dashboardRenderer = new AdminDashboardRenderer($getComponentSettingsUseCase);
// Crear caso de uso para renderizar
$renderDashboardUseCase = new RenderDashboardUseCase($dashboardRenderer);
// Crear y registrar el menú de administración
$adminMenuRegistrar = new AdminMenuRegistrar($menuItem, $renderDashboardUseCase);
$adminMenuRegistrar->register();
// Crear y registrar el enqueuer de assets
$adminAssetEnqueuer = new AdminAssetEnqueuer(get_template_directory_uri());
$adminAssetEnqueuer->register();
// Obtener Use Case para guardar configuraciones
$saveComponentSettingsUseCase = $container?->getSaveComponentSettingsUseCase();
// Crear y registrar el handler AJAX con inyección del Use Case
$adminAjaxHandler = new \ROITheme\Admin\Infrastructure\API\WordPress\AdminAjaxHandler($saveComponentSettingsUseCase);
$adminAjaxHandler->register();
// Crear y registrar el handler AJAX para el Contact Form (público)
$contactFormAjaxHandler = new \ROITheme\Public\ContactForm\Infrastructure\Api\WordPress\ContactFormAjaxHandler(
$container->getComponentSettingsRepository()
);
$contactFormAjaxHandler->register();
// Crear y registrar el handler AJAX para Newsletter (público)
$newsletterAjaxHandler = new \ROITheme\Public\Footer\Infrastructure\Api\WordPress\NewsletterAjaxHandler(
$container->getComponentSettingsRepository()
);
$newsletterAjaxHandler->register();
// Log en modo debug
if (defined('WP_DEBUG') && WP_DEBUG) {
error_log('ROI Theme: Admin Panel initialized successfully');
}
} catch (\Throwable $e) {
// Manejar errores de inicialización del panel
if (defined('WP_DEBUG') && WP_DEBUG) {
error_log('ROI Theme: Failed to initialize Admin Panel: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
}
} }
/** // =============================================================================
* ======================================================================== // 4. CONFIGURACIÓN DEL TEMA
* END BOOTSTRAP // =============================================================================
* ========================================================================
*/
/** /**
* ======================================================================== * Setup del tema
* THEME DATABASE TABLES SETUP
* ========================================================================
* *
* Crea las tablas del tema cuando se activa. * Configuraciones básicas de WordPress theme support
* Esto asegura que el tema sea portable y funcione en cualquier instalación WordPress.
*/ */
add_action('after_setup_theme', function() {
// Soporte para título del documento
add_theme_support('title-tag');
// Soporte para imágenes destacadas
add_theme_support('post-thumbnails');
// Soporte para HTML5
add_theme_support('html5', [
'search-form',
'comment-form',
'comment-list',
'gallery',
'caption',
'style',
'script'
]);
// Soporte para feeds automáticos
add_theme_support('automatic-feed-links');
// Registro de ubicaciones de menús
register_nav_menus([
'primary' => __('Primary Menu', 'roi-theme'),
'footer' => __('Footer Menu', 'roi-theme'),
'footer_menu_1' => __('Footer Menu 1 (Widget 1)', 'roi-theme'),
'footer_menu_2' => __('Footer Menu 2 (Widget 2)', 'roi-theme'),
'footer_menu_3' => __('Footer Menu 3 (Widget 3)', 'roi-theme'),
]);
// TODO: Agregar más configuraciones según sea necesario
});
// =============================================================================
// 5. HOOKS DE INICIALIZACIÓN (Para fases posteriores)
// =============================================================================
/**
* Hook para sincronización de schemas
* TODO: Implementar en Fase 6
*/
// add_action('admin_init', function() use ($container) {
// $syncService = $container->getSchemaSyncService();
// // Verificar si hay schemas desactualizados
// });
/**
* Hook para detección de schemas desactualizados
* TODO: Implementar en Fase 6
*/
// add_action('admin_notices', function() use ($container) {
// // Mostrar aviso si hay schemas desactualizados
// });
// =============================================================================
// 5. INFORMACIÓN DE DEBUG (Solo en desarrollo)
// =============================================================================
if (defined('WP_DEBUG') && WP_DEBUG) {
// Registrar que el tema se inicializó correctamente
error_log('ROI Theme: Bootstrap completed successfully');
}
// =============================================================================
// 6. INSTALACIÓN DE TABLAS DEL TEMA
// =============================================================================
/** /**
* Crear tablas del tema en la activación * Crear tablas del tema en la activación
* *
* Este hook se ejecuta cuando el tema se activa en WordPress. * Este hook se ejecuta cuando el tema se activa en WordPress.
* Crea las tablas necesarias si no existen. * Crea las tablas necesarias si no existen.
*
* @since 1.0.19
*/ */
add_action('after_switch_theme', function() { add_action('after_switch_theme', function() {
global $wpdb; global $wpdb;
@@ -68,39 +242,25 @@ add_action('after_switch_theme', function() {
$charset_collate = $wpdb->get_charset_collate(); $charset_collate = $wpdb->get_charset_collate();
// Tabla de components // Tabla de configuración de componentes (normalizada)
$table_components = $wpdb->prefix . 'roi_theme_components'; $table_settings = $wpdb->prefix . 'roi_theme_component_settings';
$sql_components = "CREATE TABLE {$table_components} ( $sql_settings = "CREATE TABLE {$table_settings} (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
component_name VARCHAR(50) NOT NULL, component_name VARCHAR(100) NOT NULL,
configuration LONGTEXT NOT NULL, group_name VARCHAR(100) NOT NULL,
content LONGTEXT, attribute_name VARCHAR(100) NOT NULL,
visibility TEXT NOT NULL, attribute_value LONGTEXT NOT NULL,
is_enabled TINYINT(1) NOT NULL DEFAULT 1, is_editable TINYINT(1) NOT NULL DEFAULT 1,
schema_version VARCHAR(20) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id), PRIMARY KEY (id),
UNIQUE KEY component_name (component_name), UNIQUE KEY unique_setting (component_name, group_name, attribute_name),
INDEX idx_enabled (is_enabled), INDEX idx_component (component_name),
INDEX idx_schema_version (schema_version) INDEX idx_editable (is_editable)
) {$charset_collate};"; ) {$charset_collate};";
// Tabla de defaults/schemas // Crear/actualizar tabla
$table_defaults = $wpdb->prefix . 'roi_theme_defaults'; dbDelta($sql_settings);
$sql_defaults = "CREATE TABLE {$table_defaults} (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
component_name VARCHAR(50) NOT NULL,
default_schema LONGTEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY component_name (component_name)
) {$charset_collate};";
// Crear/actualizar tablas
dbDelta($sql_components);
dbDelta($sql_defaults);
// Log en modo debug // Log en modo debug
if (defined('WP_DEBUG') && WP_DEBUG) { if (defined('WP_DEBUG') && WP_DEBUG) {
@@ -108,268 +268,11 @@ add_action('after_switch_theme', function() {
} }
}); });
/**
* Theme Version
*/
define('ROI_VERSION', '1.0.19');
/**
* Theme Setup
*/
function roi_theme_setup() {
// Make theme available for translation
load_theme_textdomain('roi-theme', get_template_directory() . '/languages');
// Let WordPress manage the document title
add_theme_support('title-tag');
// Enable support for Post Thumbnails
add_theme_support('post-thumbnails');
// Add image sizes
add_image_size('roi-thumbnail', 400, 300, true);
add_image_size('roi-medium', 800, 600, true);
add_image_size('roi-large', 1200, 900, true);
add_image_size('roi-featured-large', 1200, 600, true);
add_image_size('roi-featured-medium', 800, 400, true);
// Switch default core markup to output valid HTML5
add_theme_support('html5', array(
'gallery',
'caption',
'style',
'script',
));
// Set content width
if (!isset($content_width)) {
$content_width = 1200;
}
// Register navigation menus
register_nav_menus(array(
'primary' => __('Primary Menu', 'roi-theme'),
'footer' => __('Footer Menu', 'roi-theme'),
));
}
add_action('after_setup_theme', 'roi_theme_setup');
/**
* Set the content width in pixels
*/
function roi_content_width() {
$GLOBALS['content_width'] = apply_filters('roi_content_width', 1200);
}
add_action('after_setup_theme', 'roi_content_width', 0);
/**
* ELIMINADO: roi_enqueue_scripts()
*
* Esta función estaba duplicando la carga de CSS.
* El sistema modular en inc/enqueue-scripts.php ya carga style.css como 'roi-main-style' (prioridad 5).
* Esta función duplicada lo cargaba otra vez como 'roi-theme-style' (prioridad 10).
*
* Fecha eliminación: 2025-01-08
* Issue: #128 - Footer Contact Form
*/
/**
* Register Widget Areas
*/
function roi_register_widget_areas() {
// Primary Sidebar
register_sidebar(array(
'name' => __('Primary Sidebar', 'roi-theme'),
'id' => 'sidebar-1',
'description' => __('Main sidebar widget area', 'roi-theme'),
'before_widget' => '<section id="%1$s" class="widget %2$s">',
'after_widget' => '</section>',
'before_title' => '<h2 class="widget-title">',
'after_title' => '</h2>',
));
// Footer Contact Form (Issue #37) - ARRIBA de los 4 widgets
register_sidebar(array(
'name' => __('Footer Contact Form', 'roi-theme'),
'id' => 'footer-contact',
'description' => __('Área de contacto arriba de los 4 widgets del footer', 'roi-theme'),
'before_widget' => '<section id="%1$s" class="footer-contact-widget %2$s">',
'after_widget' => '</section>',
'before_title' => '<h3 class="widget-title">',
'after_title' => '</h3>',
));
// Footer Widget Areas
for ($i = 1; $i <= 4; $i++) {
register_sidebar(array(
'name' => sprintf(__('Footer Column %d', 'roi-theme'), $i),
'id' => 'footer-' . $i,
'description' => sprintf(__('Footer widget area %d', 'roi-theme'), $i),
'before_widget' => '<div id="%1$s" class="widget %2$s">',
'after_widget' => '</div>',
'before_title' => '<h3 class="widget-title">',
'after_title' => '</h3>',
));
}
}
add_action('widgets_init', 'roi_register_widget_areas');
/**
* Configure locale and date format
*/
function roi_configure_locale() {
// Set locale to es_MX
add_filter('locale', function($locale) {
return 'es_MX';
});
}
add_action('after_setup_theme', 'roi_configure_locale');
/**
* Custom date format
*/
function roi_custom_date_format($format) {
return 'd/m/Y'; // Format: day/month/year
}
add_filter('date_format', 'roi_custom_date_format');
/**
* Include modular files
*/
// Sanitize Functions (load first to avoid redeclaration errors)
if (file_exists(get_template_directory() . '/inc/sanitize-functions.php')) {
require_once get_template_directory() . '/inc/sanitize-functions.php';
}
// Theme Options Helpers (load first as other files may depend on it)
if (file_exists(get_template_directory() . '/inc/theme-options-helpers.php')) {
require_once get_template_directory() . '/inc/theme-options-helpers.php';
}
// Admin Options API (Theme Options)
// Cargar solo options-api.php para funciones auxiliares como roi_get_default_options()
// theme-options.php está desactivado porque el menú se registra en admin/includes/class-admin-menu.php
if (is_admin()) {
if (file_exists(get_template_directory() . '/admin/theme-options/options-api.php')) {
require_once get_template_directory() . '/admin/theme-options/options-api.php';
}
}
// Bootstrap Nav Walker
if (file_exists(get_template_directory() . '/inc/nav-walker.php')) {
require_once get_template_directory() . '/inc/nav-walker.php';
}
// Bootstrap and Script Enqueuing
if (file_exists(get_template_directory() . '/inc/enqueue-scripts.php')) {
require_once get_template_directory() . '/inc/enqueue-scripts.php';
}
// Font customizer options
if (file_exists(get_template_directory() . '/inc/customizer-fonts.php')) {
require_once get_template_directory() . '/inc/customizer-fonts.php';
}
// SEO optimizations and Rank Math compatibility
if (file_exists(get_template_directory() . '/inc/seo.php')) {
require_once get_template_directory() . '/inc/seo.php';
}
// Performance optimizations
if (file_exists(get_template_directory() . '/inc/performance.php')) {
require_once get_template_directory() . '/inc/performance.php';
}
// Critical CSS (optional, disabled by default)
if (file_exists(get_template_directory() . '/inc/critical-css.php')) {
require_once get_template_directory() . '/inc/critical-css.php';
}
// Image optimization
if (file_exists(get_template_directory() . '/inc/image-optimization.php')) {
require_once get_template_directory() . '/inc/image-optimization.php';
}
// Template functions
if (file_exists(get_template_directory() . '/inc/template-functions.php')) {
require_once get_template_directory() . '/inc/template-functions.php';
}
// Template tags
if (file_exists(get_template_directory() . '/inc/template-tags.php')) {
require_once get_template_directory() . '/inc/template-tags.php';
}
// Featured image functions
if (file_exists(get_template_directory() . '/inc/featured-image.php')) {
require_once get_template_directory() . '/inc/featured-image.php';
}
// Category badge functions
if (file_exists(get_template_directory() . '/inc/category-badge.php')) {
require_once get_template_directory() . '/inc/category-badge.php';
}
// AdSense delay loading
if (file_exists(get_template_directory() . '/inc/adsense-delay.php')) {
require_once get_template_directory() . '/inc/adsense-delay.php';
}
// Related posts functionality
if (file_exists(get_template_directory() . '/inc/related-posts.php')) {
require_once get_template_directory() . '/inc/related-posts.php';
}
// Related posts configuration options (admin helpers)
if (file_exists(get_template_directory() . '/admin/theme-options/related-posts-options.php')) {
require_once get_template_directory() . '/admin/theme-options/related-posts-options.php';
}
// Table of Contents
if (file_exists(get_template_directory() . '/inc/toc.php')) {
require_once get_template_directory() . '/inc/toc.php';
}
// APU Tables - Funciones para tablas de Análisis de Precios Unitarios (Issue #30)
if (file_exists(get_template_directory() . '/inc/apu-tables.php')) {
require_once get_template_directory() . '/inc/apu-tables.php';
}
// Desactivar búsqueda nativa (Issue #3)
if (file_exists(get_template_directory() . '/inc/search-disable.php')) {
require_once get_template_directory() . '/inc/search-disable.php';
}
// Desactivar comentarios (Issue #4)
if (file_exists(get_template_directory() . '/inc/comments-disable.php')) {
require_once get_template_directory() . '/inc/comments-disable.php';
}
// Social share buttons (Issue #31)
if (file_exists(get_template_directory() . '/inc/social-share.php')) {
require_once get_template_directory() . '/inc/social-share.php';
}
// CTA A/B Testing system (Issue #32)
if (file_exists(get_template_directory() . '/inc/cta-ab-testing.php')) {
require_once get_template_directory() . '/inc/cta-ab-testing.php';
}
// CTA Customizer options (Issue #32)
if (file_exists(get_template_directory() . '/inc/customizer-cta.php')) {
require_once get_template_directory() . '/inc/customizer-cta.php';
}
// Admin Panel Module (Phase 1-2: Base Structure)
if (file_exists(get_template_directory() . '/admin/init.php')) {
require_once get_template_directory() . '/admin/init.php';
}
// ============================================================================= // =============================================================================
// REGISTRO DE COMANDOS WP-CLI // REGISTRO DE COMANDOS WP-CLI
// ============================================================================= // =============================================================================
if (defined('WP_CLI') && WP_CLI) { if (defined('WP_CLI') && WP_CLI) {
require_once get_template_directory() . '/src/Infrastructure/API/WordPress/MigrationCommand.php'; require_once get_template_directory() . '/Shared/Infrastructure/Api/WordPress/MigrationCommand.php';
} }

View File

@@ -20,13 +20,44 @@
<body <?php body_class(); ?> data-bs-spy="scroll" data-bs-target=".toc-container" data-bs-offset="100"> <body <?php body_class(); ?> data-bs-spy="scroll" data-bs-target=".toc-container" data-bs-offset="100">
<?php wp_body_open(); ?> <?php wp_body_open(); ?>
<?php get_template_part('template-parts/top-notification-bar'); ?> <?php
if (function_exists('roi_render_component')) {
echo roi_render_component('top-notification-bar');
}
?>
<!-- Navbar (Template líneas 264-320) --> <!-- Navbar (Template líneas 264-320) -->
<nav class="navbar navbar-expand-lg navbar-dark py-3" role="navigation" aria-label="<?php esc_attr_e('Primary Navigation', 'roi-theme'); ?>"> <nav class="navbar navbar-expand-lg navbar-dark py-3" role="navigation" aria-label="<?php esc_attr_e('Primary Navigation', 'roi-theme'); ?>">
<div class="container"> <div class="container">
<!-- Hamburger Toggle Button - PRIMERO según template línea 286 --> <?php
// Brand/Logo - Leer configuración desde BD
$show_brand = roi_get_navbar_setting('media', 'show_brand', false);
if ($show_brand) :
$use_logo = roi_get_navbar_setting('media', 'use_logo', false);
$brand_text = roi_get_navbar_setting('media', 'brand_text', get_bloginfo('name'));
$home_url = home_url('/');
if ($use_logo) :
$logo_url = roi_get_navbar_setting('media', 'logo_url', '');
if (!empty($logo_url)) :
?>
<a class="navbar-brand" href="<?php echo esc_url($home_url); ?>">
<img src="<?php echo esc_url($logo_url); ?>" alt="<?php echo esc_attr($brand_text); ?>" class="roi-navbar-logo">
</a>
<?php
endif;
else :
?>
<a class="navbar-brand roi-navbar-brand" href="<?php echo esc_url($home_url); ?>">
<?php echo esc_html($brand_text); ?>
</a>
<?php
endif;
endif;
?>
<!-- Hamburger Toggle Button -->
<button class="navbar-toggler" <button class="navbar-toggler"
type="button" type="button"
data-bs-toggle="collapse" data-bs-toggle="collapse"
@@ -40,38 +71,18 @@
<!-- Collapsible Menu --> <!-- Collapsible Menu -->
<div class="collapse navbar-collapse" id="navbarSupportedContent"> <div class="collapse navbar-collapse" id="navbarSupportedContent">
<?php <?php
if (has_nav_menu('primary')) { // Navbar Component - Menu de navegación
wp_nav_menu(array( if (function_exists('roi_render_component')) {
'theme_location' => 'primary', echo roi_render_component('navbar');
'container' => false,
'menu_class' => 'navbar-nav mb-2 mb-lg-0',
'fallback_cb' => false,
'depth' => 2,
'walker' => new WP_Bootstrap_Navwalker(),
));
} else {
// Fallback si no hay menú asignado
?>
<ul class="navbar-nav mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="<?php echo esc_url(home_url('/')); ?>">
<?php esc_html_e('Home', 'roi-theme'); ?>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="<?php echo esc_url(get_post_type_archive_link('post')); ?>">
<?php esc_html_e('Blog', 'roi-theme'); ?>
</a>
</li>
</ul>
<?php
} }
?> ?>
<!-- Let's Talk Button (Template líneas 315-317) --> <?php
<button class="btn btn-lets-talk ms-lg-3" type="button" data-bs-toggle="modal" data-bs-target="#contactModal"> // CTA "Let's Talk" Button Component
<i class="bi bi-lightning-charge-fill me-2"></i><?php esc_html_e('Let\'s Talk', 'roi-theme'); ?> if (function_exists('roi_render_component')) {
</button> echo roi_render_component('cta-lets-talk');
}
?>
</div> </div>
</div><!-- .container --> </div><!-- .container -->

View File

@@ -114,8 +114,8 @@ function roi_preload_custom_fonts() {
// Example preload links - uncomment and modify when you have custom font files // Example preload links - uncomment and modify when you have custom font files
/* /*
?> ?>
<link rel="preload" href="<?php echo esc_url(get_template_directory_uri() . '/assets/fonts/CustomSans-Regular.woff2'); ?>" as="font" type="font/woff2" crossorigin> <link rel="preload" href="<?php echo esc_url(get_template_directory_uri() . '/Assets/fonts/CustomSans-Regular.woff2'); ?>" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="<?php echo esc_url(get_template_directory_uri() . '/assets/fonts/CustomSans-Bold.woff2'); ?>" as="font" type="font/woff2" crossorigin> <link rel="preload" href="<?php echo esc_url(get_template_directory_uri() . '/Assets/fonts/CustomSans-Bold.woff2'); ?>" as="font" type="font/woff2" crossorigin>
<?php <?php
*/ */
} }

View File

@@ -27,7 +27,7 @@ function roi_enqueue_fonts() {
// Fonts CSS local // Fonts CSS local
wp_enqueue_style( wp_enqueue_style(
'roi-fonts', 'roi-fonts',
get_template_directory_uri() . '/assets/css/css-global-fonts.css', get_template_directory_uri() . '/Assets/css/css-global-fonts.css',
array('google-fonts-poppins'), array('google-fonts-poppins'),
'1.0.0', '1.0.0',
'all' 'all'
@@ -43,7 +43,7 @@ function roi_enqueue_bootstrap() {
// Bootstrap CSS - with high priority // Bootstrap CSS - with high priority
wp_enqueue_style( wp_enqueue_style(
'roi-bootstrap', 'roi-bootstrap',
get_template_directory_uri() . '/assets/vendor/bootstrap/css/bootstrap.min.css', get_template_directory_uri() . '/Assets/vendor/bootstrap/css/bootstrap.min.css',
array('roi-fonts'), array('roi-fonts'),
'5.3.2', '5.3.2',
'all' 'all'
@@ -52,7 +52,7 @@ function roi_enqueue_bootstrap() {
// Bootstrap Icons CSS - LOCAL (Issue #135: CORS bloqueaba CDN) // Bootstrap Icons CSS - LOCAL (Issue #135: CORS bloqueaba CDN)
wp_enqueue_style( wp_enqueue_style(
'bootstrap-icons', 'bootstrap-icons',
get_template_directory_uri() . '/assets/vendor/bootstrap-icons.min.css', get_template_directory_uri() . '/Assets/vendor/bootstrap-icons.min.css',
array('roi-bootstrap'), array('roi-bootstrap'),
'1.11.3', '1.11.3',
'all' 'all'
@@ -61,7 +61,7 @@ function roi_enqueue_bootstrap() {
// Variables CSS del Template RDash (NIVEL 1 - BLOQUEANTE - Issue #48) // Variables CSS del Template RDash (NIVEL 1 - BLOQUEANTE - Issue #48)
wp_enqueue_style( wp_enqueue_style(
'roi-variables', 'roi-variables',
get_template_directory_uri() . '/assets/css/css-global-variables.css', get_template_directory_uri() . '/Assets/css/css-global-variables.css',
array('roi-bootstrap'), array('roi-bootstrap'),
ROI_VERSION, ROI_VERSION,
'all' 'all'
@@ -70,7 +70,7 @@ function roi_enqueue_bootstrap() {
// Bootstrap JS Bundle - in footer with defer // Bootstrap JS Bundle - in footer with defer
wp_enqueue_script( wp_enqueue_script(
'roi-bootstrap-js', 'roi-bootstrap-js',
get_template_directory_uri() . '/assets/vendor/bootstrap/js/bootstrap.bundle.min.js', get_template_directory_uri() . '/Assets/vendor/bootstrap/js/bootstrap.bundle.min.js',
array(), array(),
'5.3.2', '5.3.2',
array( array(
@@ -93,7 +93,7 @@ add_action('wp_enqueue_scripts', 'roi_enqueue_bootstrap', 5);
function roi_enqueue_main_stylesheet() { function roi_enqueue_main_stylesheet() {
wp_enqueue_style( wp_enqueue_style(
'roi-main-style', 'roi-main-style',
get_template_directory_uri() . '/assets/css/style.css', get_template_directory_uri() . '/Assets/css/style.css',
array('roi-variables'), array('roi-variables'),
'1.0.5', // Arquitectura: Separación de responsabilidades CSS '1.0.5', // Arquitectura: Separación de responsabilidades CSS
'all' 'all'
@@ -106,54 +106,34 @@ add_action('wp_enqueue_scripts', 'roi_enqueue_main_stylesheet', 5);
* Enqueue FASE 2 CSS - Template RDash Component Styles (Issues #58-64) * Enqueue FASE 2 CSS - Template RDash Component Styles (Issues #58-64)
* *
* Estilos que replican componentes del template RDash * Estilos que replican componentes del template RDash
*
* NOTA: Hero Section, Post Content y Related Posts ahora usan
* estilos generados dinámicamente desde sus Renderers.
*/ */
function roi_enqueue_fase2_styles() { function roi_enqueue_fase2_styles() {
// Hero Section CSS - Gradiente azul (Issue #59) // Hero Section CSS - DESHABILITADO: estilos generados por HeroRenderer
wp_enqueue_style( // @see Public/Hero/Infrastructure/Ui/HeroRenderer.php
'roi-hero',
get_template_directory_uri() . '/assets/css/componente-hero-section.css',
array('roi-bootstrap'),
filemtime(get_template_directory() . '/assets/css/componente-hero-section.css'),
'all'
);
// Category Badges CSS - Clase genérica (Issue #62) // Category Badges CSS - Clase genérica (Issue #62)
wp_enqueue_style( wp_enqueue_style(
'roi-badges', 'roi-badges',
get_template_directory_uri() . '/assets/css/css-global-badges.css', get_template_directory_uri() . '/Assets/css/css-global-badges.css',
array('roi-bootstrap'), array('roi-bootstrap'),
filemtime(get_template_directory() . '/assets/css/css-global-badges.css'), filemtime(get_template_directory() . '/Assets/css/css-global-badges.css'),
'all' 'all'
); );
// Pagination CSS - Estilos personalizados (Issue #64) // Pagination CSS - Estilos personalizados (Issue #64)
wp_enqueue_style( wp_enqueue_style(
'roi-pagination', 'roi-pagination',
get_template_directory_uri() . '/assets/css/css-global-pagination.css', get_template_directory_uri() . '/Assets/css/css-global-pagination.css',
array('roi-bootstrap'), array('roi-bootstrap'),
filemtime(get_template_directory() . '/assets/css/css-global-pagination.css'), filemtime(get_template_directory() . '/Assets/css/css-global-pagination.css'),
'all' 'all'
); );
// Post Content Typography - Solo en posts individuales (Issue #63) // Post Content Typography y Related Posts - DESHABILITADOS
if (is_single()) { // Los estilos ahora están integrados en style.css o generados dinámicamente
wp_enqueue_style(
'roi-post-content',
get_template_directory_uri() . '/assets/css/componente-post-content.css',
array('roi-bootstrap'),
filemtime(get_template_directory() . '/assets/css/componente-post-content.css'),
'all'
);
// Related Posts CSS - Background gris (Issue #60)
wp_enqueue_style(
'roi-related-posts',
get_template_directory_uri() . '/assets/css/componente-related-posts.css',
array('roi-bootstrap'),
filemtime(get_template_directory() . '/assets/css/componente-related-posts.css'),
'all'
);
}
} }
add_action('wp_enqueue_scripts', 'roi_enqueue_fase2_styles', 6); add_action('wp_enqueue_scripts', 'roi_enqueue_fase2_styles', 6);
@@ -167,32 +147,48 @@ add_action('wp_enqueue_scripts', 'roi_enqueue_fase2_styles', 6);
* @since 1.0.7 * @since 1.0.7
*/ */
function roi_enqueue_global_components() { function roi_enqueue_global_components() {
// Notification Bar CSS - Barra superior (Issue #39) // Notification Bar CSS - DESHABILITADO: Los estilos de la barra de notificación ahora se generan
// dinámicamente desde el TopNotificationBarRenderer basado en los valores de la BD.
// @see Public/TopNotificationBar/Infrastructure/Ui/TopNotificationBarRenderer.php
/*
wp_enqueue_style( wp_enqueue_style(
'roi-notification-bar', 'roi-notification-bar',
get_template_directory_uri() . '/assets/css/componente-top-bar.css', get_template_directory_uri() . '/Assets/css/componente-top-bar.css',
array('roi-bootstrap'), array('roi-bootstrap'),
filemtime(get_template_directory() . '/assets/css/componente-top-bar.css'), filemtime(get_template_directory() . '/Assets/css/componente-top-bar.css'),
'all' 'all'
); );
*/
// Navbar CSS - Navegación principal // Navbar CSS - DESHABILITADO: Los estilos del navbar ahora se generan
// dinámicamente desde el NavbarRenderer basado en los valores de la BD.
// El archivo componente-navbar.css tenía !important que sobrescribía
// los estilos configurados por el usuario en el Admin Panel.
// @see Public/Navbar/Infrastructure/Ui/NavbarRenderer.php
/*
wp_enqueue_style( wp_enqueue_style(
'roi-navbar', 'roi-navbar',
get_template_directory_uri() . '/assets/css/componente-navbar.css', get_template_directory_uri() . '/Assets/css/componente-navbar.css',
array('roi-bootstrap'), array('roi-bootstrap'),
filemtime(get_template_directory() . '/assets/css/componente-navbar.css'), filemtime(get_template_directory() . '/Assets/css/componente-navbar.css'),
'all' 'all'
); );
*/
// Buttons CSS - Botones personalizados (Let's Talk, etc.) // Buttons CSS - DESHABILITADO: Los estilos del botón Let's Talk ahora se generan
// dinámicamente desde el CtaLetsTalkRenderer basado en los valores de la BD.
// El archivo componente-boton-lets-talk.css tenía !important que sobrescribía
// los estilos configurados por el usuario en el Admin Panel.
// @see Public/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkRenderer.php
/*
wp_enqueue_style( wp_enqueue_style(
'roi-buttons', 'roi-buttons',
get_template_directory_uri() . '/assets/css/componente-boton-lets-talk.css', get_template_directory_uri() . '/Assets/css/componente-boton-lets-talk.css',
array('roi-bootstrap'), array('roi-bootstrap'),
filemtime(get_template_directory() . '/assets/css/componente-boton-lets-talk.css'), filemtime(get_template_directory() . '/Assets/css/componente-boton-lets-talk.css'),
'all' 'all'
); );
*/
} }
add_action('wp_enqueue_scripts', 'roi_enqueue_global_components', 7); add_action('wp_enqueue_scripts', 'roi_enqueue_global_components', 7);
@@ -204,7 +200,7 @@ function roi_enqueue_header() {
// Header CSS // Header CSS
wp_enqueue_style( wp_enqueue_style(
'roi-header', 'roi-header',
get_template_directory_uri() . '/assets/css/componente-footer-principal.css', get_template_directory_uri() . '/Assets/css/componente-footer-principal.css',
array('roi-fonts'), array('roi-fonts'),
'1.0.0', '1.0.0',
'all' 'all'
@@ -213,7 +209,7 @@ function roi_enqueue_header() {
// Header JS - with defer strategy // Header JS - with defer strategy
wp_enqueue_script( wp_enqueue_script(
'roi-header-js', 'roi-header-js',
get_template_directory_uri() . '/assets/js/header.js', get_template_directory_uri() . '/Assets/js/header.js',
array(), array(),
'1.0.0', '1.0.0',
array( array(
@@ -240,7 +236,7 @@ function roi_enqueue_generic_tables() {
// Generic Tables CSS // Generic Tables CSS
wp_enqueue_style( wp_enqueue_style(
'roi-generic-tables', 'roi-generic-tables',
get_template_directory_uri() . '/assets/css/css-global-generic-tables.css', get_template_directory_uri() . '/Assets/css/css-global-generic-tables.css',
array('roi-bootstrap'), array('roi-bootstrap'),
ROI_VERSION, ROI_VERSION,
'all' 'all'
@@ -264,7 +260,7 @@ function roi_enqueue_video_styles() {
// Video CSS // Video CSS
wp_enqueue_style( wp_enqueue_style(
'roi-video', 'roi-video',
get_template_directory_uri() . '/assets/css/css-global-video.css', get_template_directory_uri() . '/Assets/css/css-global-video.css',
array('roi-bootstrap'), array('roi-bootstrap'),
ROI_VERSION, ROI_VERSION,
'all' 'all'
@@ -284,7 +280,7 @@ function roi_enqueue_main_javascript() {
// Main JavaScript - navbar scroll effects and interactions // Main JavaScript - navbar scroll effects and interactions
wp_enqueue_script( wp_enqueue_script(
'roi-main-js', 'roi-main-js',
get_template_directory_uri() . '/assets/js/main.js', get_template_directory_uri() . '/Assets/js/main.js',
array('roi-bootstrap-js'), array('roi-bootstrap-js'),
'1.0.3', // Cache bust: force remove defer with filter '1.0.3', // Cache bust: force remove defer with filter
true // Load in footer true // Load in footer
@@ -331,7 +327,7 @@ function roi_enqueue_accessibility() {
// Accessibility CSS // Accessibility CSS
wp_enqueue_style( wp_enqueue_style(
'roi-accessibility', 'roi-accessibility',
get_template_directory_uri() . '/assets/css/css-global-accessibility.css', get_template_directory_uri() . '/Assets/css/css-global-accessibility.css',
array('roi-theme-style'), array('roi-theme-style'),
ROI_VERSION, ROI_VERSION,
'all' 'all'
@@ -340,7 +336,7 @@ function roi_enqueue_accessibility() {
// Accessibility JavaScript // Accessibility JavaScript
wp_enqueue_script( wp_enqueue_script(
'roi-accessibility-js', 'roi-accessibility-js',
get_template_directory_uri() . '/assets/js/accessibility.js', get_template_directory_uri() . '/Assets/js/accessibility.js',
array('roi-bootstrap-js'), array('roi-bootstrap-js'),
ROI_VERSION, ROI_VERSION,
array( array(
@@ -375,7 +371,7 @@ function roi_enqueue_adsense_loader() {
// Enqueue del script de carga de AdSense // Enqueue del script de carga de AdSense
wp_enqueue_script( wp_enqueue_script(
'roi-adsense-loader', 'roi-adsense-loader',
get_template_directory_uri() . '/assets/js/adsense-loader.js', get_template_directory_uri() . '/Assets/js/adsense-loader.js',
array(), array(),
ROI_VERSION, ROI_VERSION,
array( array(
@@ -399,7 +395,7 @@ function roi_enqueue_theme_styles() {
// Theme Core Styles - ELIMINADO theme.css // Theme Core Styles - ELIMINADO theme.css
// wp_enqueue_style( // wp_enqueue_style(
// 'roi-theme', // 'roi-theme',
// get_template_directory_uri() . '/assets/css/theme.css', // get_template_directory_uri() . '/Assets/css/theme.css',
// array('roi-bootstrap'), // array('roi-bootstrap'),
// '1.0.0', // '1.0.0',
// 'all' // 'all'
@@ -408,7 +404,7 @@ function roi_enqueue_theme_styles() {
// Theme Animations // Theme Animations
wp_enqueue_style( wp_enqueue_style(
'roi-animations', 'roi-animations',
get_template_directory_uri() . '/assets/css/css-global-animations.css', get_template_directory_uri() . '/Assets/css/css-global-animations.css',
array('roi-bootstrap'), // Cambiado de 'roi-theme' a 'roi-bootstrap' array('roi-bootstrap'), // Cambiado de 'roi-theme' a 'roi-bootstrap'
'1.0.0', '1.0.0',
'all' 'all'
@@ -417,7 +413,7 @@ function roi_enqueue_theme_styles() {
// Theme Responsive Styles // Theme Responsive Styles
wp_enqueue_style( wp_enqueue_style(
'roi-responsive', 'roi-responsive',
get_template_directory_uri() . '/assets/css/css-global-responsive.css', get_template_directory_uri() . '/Assets/css/css-global-responsive.css',
array('roi-bootstrap'), // Cambiado de 'roi-theme' a 'roi-bootstrap' array('roi-bootstrap'), // Cambiado de 'roi-theme' a 'roi-bootstrap'
'1.0.0', '1.0.0',
'all' 'all'
@@ -426,7 +422,7 @@ function roi_enqueue_theme_styles() {
// Theme Utilities // Theme Utilities
wp_enqueue_style( wp_enqueue_style(
'roi-utilities', 'roi-utilities',
get_template_directory_uri() . '/assets/css/css-global-utilities.css', get_template_directory_uri() . '/Assets/css/css-global-utilities.css',
array('roi-bootstrap'), // Cambiado de 'roi-theme' a 'roi-bootstrap' array('roi-bootstrap'), // Cambiado de 'roi-theme' a 'roi-bootstrap'
'1.0.0', '1.0.0',
'all' 'all'
@@ -435,7 +431,7 @@ function roi_enqueue_theme_styles() {
// Print Styles // Print Styles
wp_enqueue_style( wp_enqueue_style(
'roi-print', 'roi-print',
get_template_directory_uri() . '/assets/css/css-global-print.css', get_template_directory_uri() . '/Assets/css/css-global-print.css',
array(), array(),
'1.0.0', '1.0.0',
'print' 'print'
@@ -449,7 +445,7 @@ add_action('wp_enqueue_scripts', 'roi_enqueue_theme_styles', 13);
* *
* HABILITADO: CSS de share buttons debe estar en su propio archivo * HABILITADO: CSS de share buttons debe estar en su propio archivo
* Arquitectura correcta: cada componente tiene su archivo CSS individual * Arquitectura correcta: cada componente tiene su archivo CSS individual
* Ver: wp-content/themes/roi-theme/assets/css/componente-share-buttons.css * Ver: wp-content/themes/roi-theme/Assets/css/componente-share-buttons.css
*/ */
function roi_enqueue_social_share_styles() { function roi_enqueue_social_share_styles() {
// Only enqueue on single posts // Only enqueue on single posts
@@ -460,7 +456,7 @@ function roi_enqueue_social_share_styles() {
// Social Share CSS // Social Share CSS
wp_enqueue_style( wp_enqueue_style(
'roi-social-share', 'roi-social-share',
get_template_directory_uri() . '/assets/css/componente-share-buttons.css', get_template_directory_uri() . '/Assets/css/componente-share-buttons.css',
array('roi-bootstrap'), array('roi-bootstrap'),
ROI_VERSION, ROI_VERSION,
'all' 'all'
@@ -476,7 +472,7 @@ function roi_enqueue_apu_tables_styles() {
// APU Tables CSS // APU Tables CSS
wp_enqueue_style( wp_enqueue_style(
'roi-tables-apu', 'roi-tables-apu',
get_template_directory_uri() . '/assets/css/css-tablas-apu.css', get_template_directory_uri() . '/Assets/css/css-tablas-apu.css',
array('roi-bootstrap'), array('roi-bootstrap'),
ROI_VERSION, ROI_VERSION,
'all' 'all'
@@ -497,7 +493,7 @@ function roi_enqueue_apu_tables_autoclass_script() {
// APU Tables Auto-Class JS // APU Tables Auto-Class JS
wp_enqueue_script( wp_enqueue_script(
'roi-apu-tables-autoclass', 'roi-apu-tables-autoclass',
get_template_directory_uri() . '/assets/js/apu-tables-auto-class.js', get_template_directory_uri() . '/Assets/js/apu-tables-auto-class.js',
array(), array(),
ROI_VERSION, ROI_VERSION,
array( array(
@@ -527,7 +523,7 @@ function roi_enqueue_cta_assets() {
// CTA CSS // CTA CSS
wp_enqueue_style( wp_enqueue_style(
'roi-cta-style', 'roi-cta-style',
get_template_directory_uri() . '/assets/css/componente-cta-ab-testing.css', get_template_directory_uri() . '/Assets/css/componente-cta-ab-testing.css',
array('roi-bootstrap'), array('roi-bootstrap'),
ROI_VERSION, ROI_VERSION,
'all' 'all'
@@ -536,7 +532,7 @@ function roi_enqueue_cta_assets() {
// CTA Tracking JS // CTA Tracking JS
wp_enqueue_script( wp_enqueue_script(
'roi-cta-tracking', 'roi-cta-tracking',
get_template_directory_uri() . '/assets/js/cta-tracking.js', get_template_directory_uri() . '/Assets/js/cta-tracking.js',
array(), array(),
ROI_VERSION, ROI_VERSION,
array( array(
@@ -550,63 +546,36 @@ add_action('wp_enqueue_scripts', 'roi_enqueue_cta_assets', 16);
/** /**
* Enqueue CTA Box Sidebar styles (Issue #36) * Enqueue CTA Box Sidebar styles (Issue #36)
*
* DESHABILITADO: Los estilos del CTA Box Sidebar ahora se generan
* dinámicamente desde CtaBoxSidebarRenderer basado en valores de BD.
* @see Public/CtaBoxSidebar/Infrastructure/Ui/CtaBoxSidebarRenderer.php
*/ */
function roi_enqueue_cta_box_sidebar_assets() { // function roi_enqueue_cta_box_sidebar_assets() - REMOVED
// Solo enqueue en posts individuales
if (!is_single()) {
return;
}
// CTA Box Sidebar CSS
wp_enqueue_style(
'roi-cta-box-sidebar',
get_template_directory_uri() . '/assets/css/componente-cta-box-sidebar.css',
array('roi-bootstrap'),
filemtime(get_template_directory() . '/assets/css/componente-cta-box-sidebar.css'),
'all'
);
}
add_action('wp_enqueue_scripts', 'roi_enqueue_cta_box_sidebar_assets', 17);
/** /**
* Enqueue TOC Sidebar styles (only on single posts) * Enqueue TOC Sidebar styles (only on single posts)
* *
* ARQUITECTURA: Cada componente debe tener su propio archivo CSS * DESHABILITADO: Los estilos del TOC ahora se generan
* Issue #121 - Separación de responsabilidades CSS * dinámicamente desde TableOfContentsRenderer basado en valores de BD.
* @see Public/TableOfContents/Infrastructure/Ui/TableOfContentsRenderer.php
* *
* @since 1.0.5 * @since 1.0.5
*/ */
function roi_enqueue_toc_sidebar_assets() { // function roi_enqueue_toc_sidebar_assets() - REMOVED
// Only load on single posts
if (!is_single()) {
return;
}
// TOC Sidebar CSS
wp_enqueue_style(
'roi-toc-sidebar',
get_template_directory_uri() . '/assets/css/componente-sidebar-toc.css',
array('roi-bootstrap'),
filemtime(get_template_directory() . '/assets/css/componente-sidebar-toc.css'),
'all'
);
}
add_action('wp_enqueue_scripts', 'roi_enqueue_toc_sidebar_assets', 18);
/** /**
* Enqueue Footer Contact Form styles * Enqueue Footer Contact Form styles
* *
* ARQUITECTURA CORRECTA: Cada componente debe tener su propio archivo CSS * ARQUITECTURA CORRECTA: Cada componente debe tener su propio archivo CSS
* Footer Contact Form CSS ahora está en su archivo individual * Footer Contact Form CSS ahora está en su archivo individual
* Ver: wp-content/themes/roi-theme/assets/css/componente-footer-contact-form.css * Ver: wp-content/themes/roi-theme/Assets/css/componente-footer-contact-form.css
*/ */
function roi_enqueue_footer_contact_assets() { function roi_enqueue_footer_contact_assets() {
// Footer Contact CSS // Footer Contact CSS
wp_enqueue_style( wp_enqueue_style(
'roi-footer-contact', 'roi-footer-contact',
get_template_directory_uri() . '/assets/css/componente-footer-contact-form.css', get_template_directory_uri() . '/Assets/css/componente-footer-contact-form.css',
array('roi-bootstrap'), array('roi-bootstrap'),
ROI_VERSION, ROI_VERSION,
'all' 'all'

View File

@@ -336,7 +336,7 @@ function roi_preload_critical_resources() {
); );
foreach ( $fonts as $font ) { foreach ( $fonts as $font ) {
$font_url = get_template_directory_uri() . '/assets/fonts/' . $font; $font_url = get_template_directory_uri() . '/Assets/fonts/' . $font;
printf( printf(
'<link rel="preload" href="%s" as="font" type="font/woff2" crossorigin="anonymous">' . "\n", '<link rel="preload" href="%s" as="font" type="font/woff2" crossorigin="anonymous">' . "\n",
esc_url( $font_url ) esc_url( $font_url )
@@ -344,14 +344,14 @@ function roi_preload_critical_resources() {
} }
// Preload del CSS de Bootstrap (crítico para el layout) // Preload del CSS de Bootstrap (crítico para el layout)
$bootstrap_css = get_template_directory_uri() . '/assets/vendor/bootstrap/css/bootstrap.min.css'; $bootstrap_css = get_template_directory_uri() . '/Assets/vendor/bootstrap/css/bootstrap.min.css';
printf( printf(
'<link rel="preload" href="%s" as="style">' . "\n", '<link rel="preload" href="%s" as="style">' . "\n",
esc_url( $bootstrap_css ) esc_url( $bootstrap_css )
); );
// Preload del CSS de fuentes (crítico para evitar FOIT/FOUT) // Preload del CSS de fuentes (crítico para evitar FOIT/FOUT)
$fonts_css = get_template_directory_uri() . '/assets/css/fonts.css'; $fonts_css = get_template_directory_uri() . '/Assets/css/fonts.css';
printf( printf(
'<link rel="preload" href="%s" as="style">' . "\n", '<link rel="preload" href="%s" as="style">' . "\n",
esc_url( $fonts_css ) esc_url( $fonts_css )

View File

@@ -250,7 +250,7 @@ function roi_enqueue_related_posts_styles() {
if ($enabled) { if ($enabled) {
wp_enqueue_style( wp_enqueue_style(
'roirelated-posts', 'roirelated-posts',
get_template_directory_uri() . '/assets/css/related-posts.css', get_template_directory_uri() . '/Assets/css/related-posts.css',
array('roibootstrap'), array('roibootstrap'),
ROI_VERSION, ROI_VERSION,
'all' 'all'

View File

@@ -1,106 +0,0 @@
<!-- Contact Modal -->
<div class="modal fade" id="contactModal" tabindex="-1" aria-labelledby="contactModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header border-0">
<h5 class="modal-title fw-bold" id="contactModalLabel">¿Listo para comenzar?</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cerrar modal de contacto"></button>
</div>
<div class="modal-body px-4 pb-4">
<p class="text-muted mb-4">Completa el formulario y nos pondremos en contacto contigo lo antes posible.</p>
<form id="contactForm" novalidate>
<div class="mb-3">
<label for="fullName" class="form-label">
Nombre completo <span class="text-danger" aria-label="Campo obligatorio">*</span>
</label>
<input
type="text"
class="form-control"
id="fullName"
name="fullName"
required
aria-required="true"
aria-describedby="fullNameHelp"
autocomplete="name"
>
<div class="invalid-feedback" id="fullNameHelp">
Por favor ingresa tu nombre completo
</div>
</div>
<div class="mb-3">
<label for="company" class="form-label">Empresa</label>
<input
type="text"
class="form-control"
id="company"
name="company"
aria-describedby="companyHelp"
autocomplete="organization"
>
<small id="companyHelp" class="form-text text-muted">Opcional</small>
</div>
<div class="mb-3">
<label for="whatsapp" class="form-label">
WhatsApp <span class="text-danger" aria-label="Campo obligatorio">*</span>
</label>
<input
type="tel"
class="form-control"
id="whatsapp"
name="whatsapp"
placeholder="+52 ___ ___ ____"
required
aria-required="true"
aria-describedby="whatsappHelp"
autocomplete="tel"
>
<div class="invalid-feedback" id="whatsappHelp">
Por favor ingresa un número de WhatsApp válido (10-15 dígitos)
</div>
</div>
<div class="mb-3">
<label for="email" class="form-label">
Correo electrónico <span class="text-danger" aria-label="Campo obligatorio">*</span>
</label>
<input
type="email"
class="form-control"
id="email"
name="email"
required
aria-required="true"
aria-describedby="emailHelp"
autocomplete="email"
>
<div class="invalid-feedback" id="emailHelp">
Por favor ingresa un correo electrónico válido
</div>
</div>
<div class="mb-3">
<label for="comments" class="form-label">Comentarios</label>
<textarea
class="form-control"
id="comments"
name="comments"
rows="3"
aria-describedby="commentsHelp"
></textarea>
<small id="commentsHelp" class="form-text text-muted">Opcional - Cuéntanos más sobre tu proyecto</small>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-submit-form btn-lg" aria-label="Enviar formulario de contacto">
Enviar
</button>
</div>
<div id="formMessage" class="mt-3 alert" style="display: none;" role="alert" aria-live="polite"></div>
</form>
</div>
</div>
</div>
</div>

View File

@@ -1,393 +0,0 @@
# Contexto Public - Renderizado Frontend de Componentes
## Propósito
El contexto `public/` contiene **todo el código relacionado con el renderizado de componentes en el frontend**.
Cada componente tiene su propia carpeta con su Clean Architecture completa.
## Filosofía: Context-First Architecture
Similar a `admin/`, el contexto `public/` agrupa todo lo relacionado con la visualización pública de componentes.
Cada componente es independiente y tiene sus propias capas de Clean Architecture.
## Estructura (Fase-00)
En Fase-00 solo creamos la estructura base. Los componentes se crearán en fases posteriores:
```
public/
├── README.md (este archivo)
└── .gitkeep (preserva directorio en Git)
```
## Estructura Futura (Post Fase-00)
```
public/
├── Navbar/ # Componente Navbar
│ ├── Domain/
│ │ ├── NavbarData.php # Entidad de lectura
│ │ └── NavbarDataProviderInterface.php
│ ├── Application/
│ │ ├── GetNavbarDataUseCase.php # Caso de uso
│ │ └── DTO/
│ │ └── NavbarDataDTO.php
│ ├── Infrastructure/
│ │ ├── Persistence/
│ │ │ └── WordPressNavbarDataProvider.php
│ │ ├── UI/
│ │ │ ├── NavbarRenderer.php # Renderizador
│ │ │ ├── views/
│ │ │ │ └── navbar.php # Template
│ │ │ └── assets/
│ │ │ ├── css/
│ │ │ │ └── navbar.css
│ │ │ └── js/
│ │ │ └── navbar.js
│ │ └── Hooks/
│ │ └── NavbarHooks.php # WordPress hooks
│ └── README.md
├── Footer/ # Otro componente
│ └── (misma estructura)
└── (más componentes...)
```
## Principios del Contexto Public
1. **Solo lectura**: El frontend solo lee configuraciones, no las modifica
2. **Performance**: Optimizado para carga rápida y caché
3. **Presentación**: Se enfoca en renderizar HTML, CSS, JS
4. **Independencia**: Cada componente es autónomo
## Responsabilidades
El contexto `public/` se encarga de:
**Renderizado de componentes** en el frontend
**Lectura de configuraciones** de la base de datos
**Generación de HTML** según templates
**Carga de assets** (CSS, JS) del componente
**Enqueue de scripts y estilos**
**Hooks de WordPress** para frontend (`wp_head`, `wp_footer`, etc.)
**NO** se encarga de:
- Administración de componentes (va en `admin/`)
- Guardado de configuraciones
- Lógica compartida (va en `shared/`)
## Ejemplo de Flujo de Public
### 1. Usuario visita página web
```
Usuario (navegador)
WordPress carga tema
NavbarHooks.php (registra hook 'wp_body_open')
Hook ejecutado
NavbarRenderer.php (obtiene datos y renderiza)
GetNavbarDataUseCase.php (obtiene configuración)
WordPressNavbarDataProvider.php (lee de DB)
navbar.php (template con HTML)
HTML enviado al navegador
```
### 2. Código de ejemplo
```php
// public/Navbar/Application/GetNavbarDataUseCase.php
namespace ROITheme\Public\Navbar\Application;
use ROITheme\Public\Navbar\Domain\NavbarDataProviderInterface;
final class GetNavbarDataUseCase
{
public function __construct(
private readonly NavbarDataProviderInterface $dataProvider
) {}
public function execute(): array
{
$data = $this->dataProvider->get();
// Transformar datos si es necesario
return [
'logo_url' => $data['logo_url'] ?? '',
'menu_items' => $data['menu_items'] ?? [],
'sticky' => $data['sticky'] ?? false,
];
}
}
```
```php
// public/Navbar/Infrastructure/UI/NavbarRenderer.php
namespace ROITheme\Public\Navbar\Infrastructure\UI;
use ROITheme\Public\Navbar\Application\GetNavbarDataUseCase;
final class NavbarRenderer
{
public function __construct(
private readonly GetNavbarDataUseCase $getNavbarData
) {}
public function render(): void
{
$data = $this->getNavbarData->execute();
// Encolar assets
wp_enqueue_style(
'roi-navbar',
get_template_directory_uri() . '/public/Navbar/Infrastructure/UI/assets/css/navbar.css',
[],
'1.0.0'
);
wp_enqueue_script(
'roi-navbar',
get_template_directory_uri() . '/public/Navbar/Infrastructure/UI/assets/js/navbar.js',
['jquery'],
'1.0.0',
true
);
// Renderizar template
require __DIR__ . '/views/navbar.php';
}
}
```
```php
// public/Navbar/Infrastructure/Hooks/NavbarHooks.php
namespace ROITheme\Public\Navbar\Infrastructure\Hooks;
use ROITheme\Public\Navbar\Infrastructure\UI\NavbarRenderer;
final class NavbarHooks
{
public function __construct(
private readonly NavbarRenderer $renderer
) {}
public function register(): void
{
add_action('wp_body_open', [$this->renderer, 'render']);
}
}
```
```php
// public/Navbar/Infrastructure/UI/views/navbar.php
<?php
/**
* Template del Navbar
*
* @var array $data Datos del navbar
*/
?>
<nav class="roi-navbar <?= $data['sticky'] ? 'sticky' : '' ?>">
<div class="container">
<a href="<?= home_url() ?>">
<img src="<?= esc_url($data['logo_url']) ?>" alt="Logo">
</a>
<ul class="menu">
<?php foreach ($data['menu_items'] as $item): ?>
<li>
<a href="<?= esc_url($item['url']) ?>">
<?= esc_html($item['label']) ?>
</a>
</li>
<?php endforeach; ?>
</ul>
</div>
</nav>
```
## Relación con Otros Contextos
```
admin/ → Guarda configuraciones
Base de Datos → Almacena settings
↓ lee
public/ → Renderiza componentes
Navegador del Usuario
```
**Clave**: `public/` solo LECTURA, nunca escritura.
## Optimización y Caché
### 1. Usar Transients de WordPress
```php
public function get(): array
{
$cached = get_transient('roi_navbar_data');
if ($cached !== false) {
return $cached;
}
$data = $this->fetchFromDatabase();
set_transient('roi_navbar_data', $data, HOUR_IN_SECONDS);
return $data;
}
```
### 2. Conditional Asset Loading
```php
if ($data['sticky']) {
wp_enqueue_script('roi-navbar-sticky');
}
```
### 3. Lazy Loading de Componentes
```php
add_action('wp_body_open', function() {
if (is_front_page()) {
// Solo cargar en homepage
$renderer->render();
}
});
```
## Reglas de Dependencia
**PUEDE** depender de:
- `shared/Domain/` (Value Objects, Exceptions)
- `shared/Application/` (Contracts, Services)
- `shared/Infrastructure/` (Implementaciones de servicios)
- WordPress frontend functions (`wp_enqueue_style`, `wp_head`, etc.)
**NO PUEDE** depender de:
- `admin/` (contexto independiente)
- Funciones de administración de WordPress
## Testing
### Tests Unitarios
```php
// tests/Unit/Public/Navbar/Application/GetNavbarDataUseCaseTest.php
public function test_returns_navbar_data()
{
$dataProvider = $this->createMock(NavbarDataProviderInterface::class);
$dataProvider->method('get')->willReturn(['logo_url' => 'test.png']);
$useCase = new GetNavbarDataUseCase($dataProvider);
$data = $useCase->execute();
$this->assertEquals('test.png', $data['logo_url']);
}
```
### Tests de Integración
```php
// tests/Integration/Public/Navbar/Infrastructure/WordPressNavbarDataProviderTest.php
public function test_retrieves_navbar_settings_from_db()
{
update_option('roi_navbar_settings', ['logo_url' => 'test.png']);
$provider = new WordPressNavbarDataProvider();
$data = $provider->get();
$this->assertEquals('test.png', $data['logo_url']);
}
```
### Tests E2E (Playwright)
```php
// tests/E2E/Public/NavbarRenderingTest.php
public function test_navbar_renders_on_homepage()
{
$this->visit('/');
$this->see('.roi-navbar');
$this->see('Logo');
}
```
### Tests de Performance
```php
// tests/Performance/Public/NavbarPerformanceTest.php
public function test_navbar_renders_in_less_than_100ms()
{
$start = microtime(true);
$renderer = new NavbarRenderer($this->getNavbarDataUseCase);
ob_start();
$renderer->render();
ob_end_clean();
$duration = (microtime(true) - $start) * 1000;
$this->assertLessThan(100, $duration);
}
```
## Cuándo Agregar Código Aquí
Agrega código a `public/` cuando:
- Renderizas un componente en el frontend
- Necesitas mostrar datos al usuario final
- Cargas assets (CSS, JS) para el frontend
- Registras hooks de frontend (`wp_head`, `wp_footer`, etc.)
- Optimizas performance de renderizado
No agregues aquí:
- Formularios de admin (van en `admin/`)
- Guardado de configuraciones
- Lógica compartida (va en `shared/`)
## Assets y Performance
### Organización de Assets
```
public/Navbar/Infrastructure/UI/assets/
├── css/
│ ├── navbar.css # Estilos del componente
│ └── navbar.min.css # Versión minificada
├── js/
│ ├── navbar.js # JavaScript del componente
│ └── navbar.min.js # Versión minificada
└── images/
└── default-logo.png # Imágenes del componente
```
### Minificación y Concatenación
En producción, usar versiones minificadas:
```php
$suffix = defined('SCRIPT_DEBUG') && SCRIPT_DEBUG ? '' : '.min';
wp_enqueue_style('roi-navbar', "...navbar{$suffix}.css");
```
## Estado Actual (Fase-00)
En Fase-00, `public/` solo tiene la estructura base. Los componentes se crearán en las siguientes fases:
- **Fase-1**: Estructura base e infraestructura
- **Fase-2**: Migración de base de datos
- **Fase-3**: Implementación de componentes públicos
- **Fase-4+**: Componentes adicionales y optimización
## Próximos Pasos
1. Crear primer componente en Fase-3 (ej: Navbar)
2. Implementar Domain layer (entidades de lectura)
3. Implementar Application layer (casos de uso de lectura)
4. Implementar Infrastructure layer (renderizado, hooks)
5. Crear templates y assets
6. Optimizar con caché
7. Crear tests de performance
8. Repetir para cada componente

View File

@@ -1,150 +0,0 @@
{
"component_name": "contact-form-section",
"version": "1.0.0",
"description": "Sección de contacto con información y formulario funcional mediante AJAX",
"groups": {
"section": {
"label": "Configuración de la Sección",
"priority": 10,
"fields": {
"show_section": {
"type": "boolean",
"label": "Mostrar sección",
"default": true,
"description": "Activar o desactivar la sección completa"
},
"section_title": {
"type": "text",
"label": "Título de la sección",
"default": "¿Tienes alguna pregunta?",
"required": true,
"description": "Título principal de la sección de contacto"
},
"section_subtitle": {
"type": "textarea",
"label": "Subtítulo",
"default": "Completa el formulario y nuestro equipo te responderá en menos de 24 horas.",
"description": "Descripción o subtítulo de la sección"
}
}
},
"contact_info": {
"label": "Información de Contacto",
"priority": 20,
"fields": {
"phone_enabled": {
"type": "boolean",
"label": "Mostrar teléfono",
"default": true
},
"phone_label": {
"type": "text",
"label": "Etiqueta de teléfono",
"default": "Teléfono"
},
"phone_value": {
"type": "text",
"label": "Número de teléfono",
"default": "+52 55 1234 5678"
},
"email_enabled": {
"type": "boolean",
"label": "Mostrar email",
"default": true
},
"email_label": {
"type": "text",
"label": "Etiqueta de email",
"default": "Email"
},
"email_value": {
"type": "email",
"label": "Dirección de email",
"default": "contacto@example.com"
},
"location_enabled": {
"type": "boolean",
"label": "Mostrar ubicación",
"default": true
},
"location_label": {
"type": "text",
"label": "Etiqueta de ubicación",
"default": "Ubicación"
},
"location_value": {
"type": "text",
"label": "Ubicación",
"default": "Ciudad de México, México"
}
}
},
"form": {
"label": "Configuración del Formulario",
"priority": 30,
"fields": {
"submit_button_text": {
"type": "text",
"label": "Texto del botón",
"default": "Enviar Mensaje",
"required": true,
"description": "Texto del botón de envío"
},
"submit_button_icon": {
"type": "text",
"label": "Ícono del botón",
"default": "bi-send-fill",
"description": "Clase de Bootstrap Icons"
},
"success_message": {
"type": "textarea",
"label": "Mensaje de éxito",
"default": "¡Gracias! Tu mensaje ha sido enviado correctamente. Te responderemos pronto.",
"description": "Mensaje al enviar exitosamente"
},
"error_message": {
"type": "textarea",
"label": "Mensaje de error",
"default": "Hubo un error al enviar el mensaje. Por favor, intenta de nuevo.",
"description": "Mensaje al fallar el envío"
},
"to_email": {
"type": "email",
"label": "Email de destino",
"default": "",
"description": "Email donde se recibirán los mensajes (deja vacío para usar el admin email)"
}
}
},
"styles": {
"label": "Estilos",
"priority": 40,
"fields": {
"background_color": {
"type": "text",
"label": "Clase de fondo",
"default": "bg-secondary bg-opacity-25",
"description": "Clase de Bootstrap para el fondo"
},
"icon_color": {
"type": "color",
"label": "Color de íconos",
"default": "#FF8600",
"description": "Color de los íconos de contacto"
},
"button_bg_color": {
"type": "color",
"label": "Color del botón",
"default": "#FF8600",
"description": "Color de fondo del botón"
},
"button_hover_bg": {
"type": "color",
"label": "Color del botón (hover)",
"default": "#FF6B00",
"description": "Color de fondo del botón al hover"
}
}
}
}
}

View File

@@ -1,186 +0,0 @@
{
"component_name": "contact-modal",
"version": "1.0.0",
"description": "Modal de contacto Bootstrap 5 con formulario AJAX para consultas de clientes",
"groups": {
"general": {
"label": "Configuración General",
"priority": 10,
"fields": {
"modal_title": {
"type": "text",
"label": "Título del modal",
"default": "¿Tienes alguna pregunta?",
"required": true,
"description": "Título que aparece en el encabezado del modal"
},
"modal_description": {
"type": "textarea",
"label": "Descripción",
"default": "Completa el formulario y nuestro equipo te responderá en menos de 24 horas.",
"description": "Texto descriptivo debajo del título"
}
}
},
"form_fields": {
"label": "Campos del Formulario",
"priority": 20,
"fields": {
"fullName": {
"type": "object",
"label": "Campo Nombre Completo",
"default": {
"label": "Nombre completo",
"placeholder": "",
"required": true
},
"fields": {
"label": {"type": "text", "label": "Etiqueta"},
"placeholder": {"type": "text", "label": "Placeholder"},
"required": {"type": "boolean", "label": "Requerido"}
}
},
"company": {
"type": "object",
"label": "Campo Empresa",
"default": {
"label": "Empresa",
"placeholder": "",
"required": false
},
"fields": {
"label": {"type": "text", "label": "Etiqueta"},
"placeholder": {"type": "text", "label": "Placeholder"},
"required": {"type": "boolean", "label": "Requerido"}
}
},
"whatsapp": {
"type": "object",
"label": "Campo WhatsApp",
"default": {
"label": "WhatsApp",
"placeholder": "",
"required": true
},
"fields": {
"label": {"type": "text", "label": "Etiqueta"},
"placeholder": {"type": "text", "label": "Placeholder"},
"required": {"type": "boolean", "label": "Requerido"}
}
},
"email": {
"type": "object",
"label": "Campo Email",
"default": {
"label": "Correo electrónico",
"placeholder": "",
"required": true
},
"fields": {
"label": {"type": "text", "label": "Etiqueta"},
"placeholder": {"type": "text", "label": "Placeholder"},
"required": {"type": "boolean", "label": "Requerido"}
}
},
"comments": {
"type": "object",
"label": "Campo Comentarios",
"default": {
"label": "¿En qué podemos ayudarte?",
"placeholder": "",
"required": false,
"rows": 4
},
"fields": {
"label": {"type": "text", "label": "Etiqueta"},
"placeholder": {"type": "text", "label": "Placeholder"},
"required": {"type": "boolean", "label": "Requerido"},
"rows": {"type": "number", "label": "Filas"}
}
}
}
},
"submit_button": {
"label": "Botón de Envío",
"priority": 30,
"fields": {
"text": {
"type": "text",
"label": "Texto del botón",
"default": "Enviar Mensaje",
"required": true,
"description": "Texto que aparece en el botón de envío"
},
"icon": {
"type": "text",
"label": "Ícono del botón",
"default": "bi-send-fill",
"description": "Clase de Bootstrap Icons (ej: bi-send-fill)"
}
}
},
"messages": {
"label": "Mensajes del Sistema",
"priority": 40,
"fields": {
"success": {
"type": "textarea",
"label": "Mensaje de éxito",
"default": "Mensaje enviado exitosamente. Te responderemos pronto.",
"description": "Mensaje cuando el formulario se envía correctamente"
},
"error": {
"type": "textarea",
"label": "Mensaje de error",
"default": "Error al enviar el mensaje. Por favor intenta nuevamente.",
"description": "Mensaje cuando ocurre un error"
},
"validation_error": {
"type": "textarea",
"label": "Mensaje de validación",
"default": "Por favor completa todos los campos requeridos.",
"description": "Mensaje cuando faltan campos obligatorios"
}
}
},
"settings": {
"label": "Configuración Avanzada",
"priority": 50,
"fields": {
"modal_id": {
"type": "text",
"label": "ID del modal",
"default": "contactModal",
"readonly": true,
"description": "ID HTML del modal (no modificar)"
},
"form_id": {
"type": "text",
"label": "ID del formulario",
"default": "modalContactForm",
"readonly": true,
"description": "ID HTML del formulario (no modificar)"
},
"ajax_action": {
"type": "text",
"label": "Acción AJAX",
"default": "roi_contact_modal_submit",
"readonly": true,
"description": "Nombre de la acción AJAX (no modificar)"
},
"email_to": {
"type": "email",
"label": "Email de destino",
"default": "",
"description": "Email donde se recibirán los mensajes (vacío = admin email)"
},
"email_subject": {
"type": "text",
"label": "Asunto del email",
"default": "Nuevo mensaje de contacto desde el sitio web",
"description": "Asunto del email que se enviará"
}
}
}
}
}

View File

@@ -1,208 +0,0 @@
{
"component_name": "cta-below-content",
"version": "1.0.0",
"description": "Call to Action que se muestra debajo del contenido del post",
"groups": {
"visibility": {
"label": "Visibilidad",
"priority": 10,
"fields": {
"is_enabled": {
"type": "boolean",
"label": "Activar CTA",
"default": true,
"required": true,
"description": "Activa o desactiva el componente de Call to Action"
},
"layout": {
"type": "select",
"label": "Layout del CTA",
"default": "two-column",
"options": {
"two-column": "Dos columnas (texto izquierda, botón derecha)",
"centered": "Centrado",
"stacked": "Apilado"
},
"required": true,
"description": "Distribución del contenido en el CTA"
}
}
},
"content": {
"label": "Contenido",
"priority": 20,
"fields": {
"title": {
"type": "text",
"label": "Título del CTA",
"default": "Accede a 200,000+ Análisis de Precios Unitarios",
"maxlength": 200,
"required": true,
"description": "Título principal que aparece en el CTA"
},
"subtitle": {
"type": "textarea",
"label": "Subtítulo del CTA",
"default": "Consulta estructuras completas, insumos y dosificaciones de los APUs más utilizados en construcción en México.",
"maxlength": 500,
"rows": 3,
"required": true,
"description": "Texto descriptivo que complementa el título"
}
}
},
"button": {
"label": "Configuración del Botón",
"priority": 30,
"fields": {
"button_text": {
"type": "text",
"label": "Texto del botón",
"default": "Ver Catálogo Completo",
"maxlength": 100,
"required": true,
"description": "Texto que aparece en el botón de acción"
},
"button_url": {
"type": "url",
"label": "URL del botón",
"default": "#",
"required": true,
"description": "URL de destino al hacer clic en el botón"
},
"button_target": {
"type": "select",
"label": "Abrir enlace en",
"default": "_self",
"options": {
"_self": "Misma ventana",
"_blank": "Nueva ventana"
},
"description": "Atributo target del enlace del botón"
},
"button_color": {
"type": "select",
"label": "Color del botón",
"default": "light",
"options": {
"light": "Blanco",
"dark": "Negro",
"primary": "Azul",
"success": "Verde",
"danger": "Rojo",
"warning": "Amarillo"
},
"description": "Color de fondo del botón"
},
"button_size": {
"type": "select",
"label": "Tamaño del botón",
"default": "lg",
"options": {
"sm": "Pequeño",
"md": "Mediano",
"lg": "Grande"
},
"description": "Tamaño del botón de acción"
},
"show_icon": {
"type": "boolean",
"label": "Mostrar icono en botón",
"default": true,
"description": "Muestra u oculta el icono de flecha en el botón"
},
"icon_class": {
"type": "text",
"label": "Clase del icono",
"default": "bi bi-arrow-right",
"conditional_logic": {
"field": "show_icon",
"operator": "==",
"value": true
},
"description": "Clase de Bootstrap Icons para el icono del botón"
}
}
},
"styles": {
"label": "Estilos y Colores",
"priority": 40,
"fields": {
"gradient_start_color": {
"type": "color",
"label": "Color de inicio del gradiente",
"default": "#FF8600",
"description": "Color hexadecimal de inicio del gradiente"
},
"gradient_end_color": {
"type": "color",
"label": "Color de fin del gradiente",
"default": "#FFB800",
"description": "Color hexadecimal de fin del gradiente"
},
"gradient_angle": {
"type": "number",
"label": "Ángulo del gradiente",
"default": 135,
"min": 0,
"max": 360,
"description": "Ángulo en grados del gradiente (0-360)"
},
"text_color": {
"type": "color",
"label": "Color del texto",
"default": "#FFFFFF",
"description": "Color del texto del título y subtítulo"
},
"container_classes": {
"type": "text",
"label": "Clases CSS del contenedor",
"default": "my-5 p-4 rounded cta-section",
"description": "Clases CSS adicionales para el contenedor principal"
}
}
},
"advanced": {
"label": "Configuración Avanzada",
"priority": 50,
"fields": {
"animation_enabled": {
"type": "boolean",
"label": "Activar animación de entrada",
"default": false,
"description": "Activa animaciones de entrada para hacer el CTA más llamativo"
},
"animation_type": {
"type": "select",
"label": "Tipo de animación",
"default": "fade-in",
"options": {
"fade-in": "Fade In",
"slide-up": "Slide Up",
"scale": "Scale"
},
"conditional_logic": {
"field": "animation_enabled",
"operator": "==",
"value": true
},
"description": "Tipo de animación de entrada"
},
"animation_duration": {
"type": "number",
"label": "Duración de animación (ms)",
"default": 500,
"min": 100,
"max": 3000,
"step": 50,
"conditional_logic": {
"field": "animation_enabled",
"operator": "==",
"value": true
},
"description": "Duración de la animación en milisegundos"
}
}
}
}
}

View File

@@ -1,191 +0,0 @@
{
"component_name": "cta-box-sidebar",
"version": "1.0.0",
"description": "CTA Box para sidebar con llamado a la acción destacado",
"groups": {
"content": {
"label": "Contenido del CTA",
"priority": 10,
"fields": {
"title": {
"type": "text",
"label": "Título",
"default": "¿Listo para potenciar tus proyectos?",
"maxlength": 100,
"required": true,
"description": "Título principal del CTA box"
},
"description": {
"type": "textarea",
"label": "Descripción",
"default": "Accede a nuestra biblioteca completa de APUs y herramientas profesionales.",
"maxlength": 200,
"required": true,
"description": "Texto descriptivo del CTA"
}
}
},
"button": {
"label": "Configuración del Botón",
"priority": 20,
"fields": {
"button_text": {
"type": "text",
"label": "Texto del botón",
"default": "Solicitar Demo",
"maxlength": 50,
"required": true,
"description": "Texto que aparece en el botón CTA"
},
"button_icon": {
"type": "text",
"label": "Ícono del botón",
"default": "bi-calendar-check",
"description": "Clase de Bootstrap Icons (ej: bi-calendar-check)"
},
"button_action": {
"type": "select",
"label": "Acción del botón",
"default": "modal",
"options": {
"modal": "Abrir Modal",
"link": "Ir a URL",
"custom": "JavaScript Personalizado"
},
"required": true,
"description": "Tipo de acción al hacer clic"
},
"modal_target": {
"type": "text",
"label": "ID del modal",
"default": "#contactModal",
"description": "ID del modal a abrir (si button_action es 'modal')"
},
"link_url": {
"type": "url",
"label": "URL de destino",
"default": "",
"description": "URL del enlace (si button_action es 'link')"
},
"link_target": {
"type": "select",
"label": "Abrir enlace en",
"default": "_self",
"options": {
"_self": "Misma pestaña",
"_blank": "Nueva pestaña"
},
"description": "Target del enlace"
},
"custom_onclick": {
"type": "textarea",
"label": "JavaScript personalizado",
"default": "",
"description": "Código JavaScript para onclick (si button_action es 'custom')"
}
}
},
"config": {
"label": "Configuración General",
"priority": 30,
"fields": {
"height": {
"type": "text",
"label": "Altura del CTA box",
"default": "250px",
"description": "Altura del CTA box (CSS válido)"
},
"show_on_mobile": {
"type": "boolean",
"label": "Mostrar en móviles",
"default": true,
"description": "Mostrar en dispositivos móviles"
},
"custom_css_class": {
"type": "text",
"label": "Clase CSS personalizada",
"default": "",
"description": "Clase CSS adicional para el contenedor"
}
}
},
"styles": {
"label": "Estilos de Color",
"priority": 40,
"fields": {
"background_gradient": {
"type": "boolean",
"label": "Usar gradiente",
"default": false,
"description": "Usar gradiente en vez de color sólido"
},
"background_color": {
"type": "color",
"label": "Color de fondo",
"default": "#FF8600",
"description": "Color de fondo del CTA box"
},
"gradient_start": {
"type": "color",
"label": "Color inicial del gradiente",
"default": "#FF8600",
"description": "Color inicial del gradiente"
},
"gradient_end": {
"type": "color",
"label": "Color final del gradiente",
"default": "#FF6B00",
"description": "Color final del gradiente"
},
"title_color": {
"type": "color",
"label": "Color del título",
"default": "#ffffff",
"description": "Color del texto del título"
},
"description_color": {
"type": "text",
"label": "Color de la descripción",
"default": "rgba(255, 255, 255, 0.95)",
"description": "Color del texto de la descripción"
},
"button_bg_color": {
"type": "color",
"label": "Color de fondo del botón",
"default": "#ffffff",
"description": "Color de fondo del botón"
},
"button_text_color": {
"type": "color",
"label": "Color del texto del botón",
"default": "#FF8600",
"description": "Color del texto del botón"
},
"button_hover_bg": {
"type": "color",
"label": "Color de fondo del botón (hover)",
"default": "#0E2337",
"description": "Color de fondo del botón al hover"
},
"button_hover_text": {
"type": "color",
"label": "Color del texto del botón (hover)",
"default": "#ffffff",
"description": "Color del texto del botón al hover"
},
"shadow_color": {
"type": "text",
"label": "Color de sombra",
"default": "rgba(255, 133, 0, 0.2)",
"description": "Color de la sombra"
},
"border_radius": {
"type": "text",
"label": "Radio del borde",
"default": "8px",
"description": "Radio del borde (CSS válido)"
}
}
}
}
}

View File

@@ -1,220 +0,0 @@
{
"component_name": "footer",
"version": "1.0.0",
"description": "Footer completo del sitio con 3 widgets, newsletter, copyright y redes sociales",
"groups": {
"widget_1": {
"label": "Widget 1",
"priority": 10,
"fields": {
"enabled": {
"type": "boolean",
"label": "Activar widget 1",
"default": true,
"description": "Mostrar u ocultar este widget"
},
"title": {
"type": "text",
"label": "Título del widget",
"default": "Recursos",
"description": "Título de la columna del widget"
},
"links": {
"type": "repeater",
"label": "Enlaces",
"description": "Lista de enlaces del widget",
"default": [
{"text": "Home", "url": "/"},
{"text": "Features", "url": "#features"},
{"text": "Pricing", "url": "#pricing"},
{"text": "FAQs", "url": "#faqs"},
{"text": "About", "url": "#about"}
],
"fields": {
"text": {
"type": "text",
"label": "Texto del enlace",
"required": true
},
"url": {
"type": "text",
"label": "URL",
"required": true
}
}
}
}
},
"widget_2": {
"label": "Widget 2",
"priority": 20,
"fields": {
"enabled": {
"type": "boolean",
"label": "Activar widget 2",
"default": true
},
"title": {
"type": "text",
"label": "Título del widget",
"default": "Servicios"
},
"links": {
"type": "repeater",
"label": "Enlaces",
"default": [
{"text": "Análisis", "url": "#analisis"},
{"text": "Presupuestos", "url": "#presupuestos"},
{"text": "Cotizaciones", "url": "#cotizaciones"},
{"text": "Proyectos", "url": "#proyectos"}
],
"fields": {
"text": {"type": "text", "label": "Texto", "required": true},
"url": {"type": "text", "label": "URL", "required": true}
}
}
}
},
"widget_3": {
"label": "Widget 3",
"priority": 30,
"fields": {
"enabled": {
"type": "boolean",
"label": "Activar widget 3",
"default": true
},
"title": {
"type": "text",
"label": "Título del widget",
"default": "Empresa"
},
"links": {
"type": "repeater",
"label": "Enlaces",
"default": [
{"text": "Acerca de", "url": "#acerca"},
{"text": "Blog", "url": "/blog"},
{"text": "Contacto", "url": "#contacto"},
{"text": "Política de Privacidad", "url": "/privacidad"}
],
"fields": {
"text": {"type": "text", "label": "Texto", "required": true},
"url": {"type": "text", "label": "URL", "required": true}
}
}
}
},
"newsletter": {
"label": "Newsletter",
"priority": 40,
"fields": {
"enabled": {
"type": "boolean",
"label": "Activar newsletter",
"default": true,
"description": "Mostrar u ocultar sección de newsletter"
},
"title": {
"type": "text",
"label": "Título",
"default": "Suscríbete a nuestro newsletter",
"required": true,
"description": "Título de la sección de newsletter"
},
"description": {
"type": "textarea",
"label": "Descripción",
"default": "Recibe actualizaciones mensuales sobre nuestros productos y servicios.",
"description": "Texto descriptivo debajo del título"
},
"placeholder": {
"type": "text",
"label": "Placeholder del email",
"default": "Correo electrónico",
"description": "Texto placeholder del campo de email"
},
"button_text": {
"type": "text",
"label": "Texto del botón",
"default": "Suscribirse",
"required": true,
"description": "Texto del botón de suscripción"
}
}
},
"copyright": {
"label": "Copyright",
"priority": 50,
"fields": {
"text": {
"type": "text",
"label": "Texto de copyright",
"default": "ROI Theme. Todos los derechos reservados.",
"required": true,
"description": "Texto que aparece después del año"
},
"year_auto": {
"type": "boolean",
"label": "Año automático",
"default": true,
"description": "Mostrar el año actual automáticamente"
}
}
},
"social_links": {
"label": "Redes Sociales",
"priority": 60,
"fields": {
"twitter": {
"type": "text",
"label": "Twitter URL",
"default": "",
"description": "URL completa de perfil de Twitter (deja vacío para ocultar)"
},
"instagram": {
"type": "text",
"label": "Instagram URL",
"default": "",
"description": "URL completa de perfil de Instagram"
},
"facebook": {
"type": "text",
"label": "Facebook URL",
"default": "",
"description": "URL completa de página de Facebook"
},
"linkedin": {
"type": "text",
"label": "LinkedIn URL",
"default": "",
"description": "URL completa de perfil o página de LinkedIn"
}
}
},
"styles": {
"label": "Estilos",
"priority": 70,
"fields": {
"background_color": {
"type": "text",
"label": "Clase de fondo",
"default": "bg-dark",
"description": "Clase de Bootstrap para el fondo (ej: bg-dark, bg-secondary)"
},
"text_color": {
"type": "text",
"label": "Clase de color de texto",
"default": "text-white",
"description": "Clase de Bootstrap para el color de texto"
},
"link_hover_color": {
"type": "color",
"label": "Color de enlaces al hover",
"default": "#FF8600",
"description": "Color de los enlaces cuando se pasa el mouse sobre ellos"
}
}
}
}
}

View File

@@ -1,410 +0,0 @@
{
"component_name": "hero-section",
"version": "1.0.0",
"description": "Sección hero con badges de categorías y título H1 con gradiente",
"groups": {
"visibility": {
"label": "Visibilidad",
"priority": 10,
"fields": {
"is_enabled": {
"type": "boolean",
"label": "Mostrar hero section",
"default": true,
"required": true,
"description": "Activa o desactiva la sección hero"
},
"show_on_pages": {
"type": "select",
"label": "Mostrar en",
"default": "posts",
"options": {
"all": "Todas las páginas",
"home": "Solo página de inicio",
"posts": "Solo posts individuales",
"pages": "Solo páginas",
"custom": "Tipos de post específicos"
},
"required": true,
"description": "Define en qué páginas se mostrará la hero section"
},
"custom_post_types": {
"type": "text",
"label": "Tipos de post personalizados",
"default": "",
"placeholder": "Ej: post,page,producto",
"conditional_logic": {
"field": "show_on_pages",
"operator": "==",
"value": "custom"
},
"description": "Slugs de tipos de post separados por comas"
}
}
},
"categories": {
"label": "Badges de Categorías",
"priority": 20,
"fields": {
"show_categories": {
"type": "boolean",
"label": "Mostrar badges de categorías",
"default": true,
"description": "Muestra badges con las categorías del post"
},
"categories_source": {
"type": "select",
"label": "Fuente de categorías",
"default": "post_categories",
"options": {
"post_categories": "Categorías del post",
"post_tags": "Etiquetas del post",
"custom_taxonomy": "Taxonomía personalizada",
"custom_list": "Lista personalizada"
},
"conditional_logic": {
"field": "show_categories",
"operator": "==",
"value": true
},
"required": true,
"description": "Define de dónde obtener las categorías"
},
"custom_taxonomy_name": {
"type": "text",
"label": "Nombre de taxonomía personalizada",
"default": "",
"placeholder": "Ej: project_category",
"conditional_logic": {
"field": "categories_source",
"operator": "==",
"value": "custom_taxonomy"
},
"description": "Slug de la taxonomía personalizada"
},
"custom_categories_list": {
"type": "textarea",
"label": "Lista personalizada de categorías",
"default": "",
"placeholder": "Análisis de Precios|#\nConstrucción|#\nMateriales|#",
"rows": 5,
"conditional_logic": {
"field": "categories_source",
"operator": "==",
"value": "custom_list"
},
"description": "Una categoría por línea en formato: Nombre|URL"
},
"max_categories": {
"type": "number",
"label": "Máximo de categorías a mostrar",
"default": 5,
"min": 1,
"max": 20,
"conditional_logic": {
"field": "show_categories",
"operator": "==",
"value": true
},
"description": "Número máximo de badges a mostrar"
},
"category_icon": {
"type": "text",
"label": "Ícono de categoría",
"default": "bi-folder-fill",
"placeholder": "Ej: bi-folder-fill",
"conditional_logic": {
"field": "show_categories",
"operator": "==",
"value": true
},
"description": "Clase del ícono Bootstrap para los badges"
},
"categories_alignment": {
"type": "select",
"label": "Alineación de categorías",
"default": "center",
"options": {
"left": "Izquierda",
"center": "Centro",
"right": "Derecha"
},
"conditional_logic": {
"field": "show_categories",
"operator": "==",
"value": true
},
"description": "Alineación de los badges de categorías"
}
}
},
"title": {
"label": "Título Principal",
"priority": 30,
"fields": {
"title_source": {
"type": "select",
"label": "Fuente del título",
"default": "post_title",
"options": {
"post_title": "Título del post",
"custom_field": "Campo personalizado",
"custom_text": "Texto personalizado"
},
"required": true,
"description": "Define de dónde obtener el texto del título"
},
"custom_field_name": {
"type": "text",
"label": "Nombre del campo personalizado",
"default": "",
"placeholder": "Ej: hero_title",
"conditional_logic": {
"field": "title_source",
"operator": "==",
"value": "custom_field"
},
"description": "Nombre del custom field de WordPress"
},
"custom_text": {
"type": "textarea",
"label": "Texto personalizado",
"default": "",
"rows": 3,
"maxlength": 500,
"conditional_logic": {
"field": "title_source",
"operator": "==",
"value": "custom_text"
},
"description": "Texto personalizado para el título"
},
"title_tag": {
"type": "select",
"label": "Etiqueta HTML del título",
"default": "h1",
"options": {
"h1": "H1",
"h2": "H2",
"h3": "H3",
"div": "DIV"
},
"description": "Etiqueta HTML para el título"
},
"title_classes": {
"type": "text",
"label": "Clases CSS adicionales",
"default": "display-5 fw-bold",
"placeholder": "Ej: display-5 fw-bold",
"description": "Clases CSS adicionales para el título"
},
"title_alignment": {
"type": "select",
"label": "Alineación del título",
"default": "center",
"options": {
"left": "Izquierda",
"center": "Centro",
"right": "Derecha"
},
"description": "Alineación del título"
},
"enable_gradient": {
"type": "boolean",
"label": "Activar gradiente en el texto",
"default": false,
"description": "Aplica efecto de gradiente al texto del título"
},
"gradient_color_start": {
"type": "color",
"label": "Color inicial del gradiente",
"default": "#1e3a5f",
"conditional_logic": {
"field": "enable_gradient",
"operator": "==",
"value": true
},
"description": "Color de inicio del gradiente"
},
"gradient_color_end": {
"type": "color",
"label": "Color final del gradiente",
"default": "#FF8600",
"conditional_logic": {
"field": "enable_gradient",
"operator": "==",
"value": true
},
"description": "Color final del gradiente"
},
"gradient_direction": {
"type": "select",
"label": "Dirección del gradiente",
"default": "to-right",
"options": {
"to-right": "Izquierda a derecha",
"to-left": "Derecha a izquierda",
"to-bottom": "Arriba a abajo",
"to-top": "Abajo a arriba",
"diagonal": "Diagonal"
},
"conditional_logic": {
"field": "enable_gradient",
"operator": "==",
"value": true
},
"description": "Dirección del gradiente"
}
}
},
"styles": {
"label": "Estilos",
"priority": 40,
"fields": {
"background_type": {
"type": "select",
"label": "Tipo de fondo",
"default": "gradient",
"options": {
"color": "Color sólido",
"gradient": "Gradiente",
"image": "Imagen",
"none": "Sin fondo"
},
"required": true,
"description": "Tipo de fondo para la hero section"
},
"background_color": {
"type": "color",
"label": "Color de fondo",
"default": "#1e3a5f",
"conditional_logic": {
"field": "background_type",
"operator": "==",
"value": "color"
},
"description": "Color sólido de fondo"
},
"gradient_start_color": {
"type": "color",
"label": "Color inicial del gradiente",
"default": "#1e3a5f",
"conditional_logic": {
"field": "background_type",
"operator": "==",
"value": "gradient"
},
"description": "Color de inicio del gradiente de fondo"
},
"gradient_end_color": {
"type": "color",
"label": "Color final del gradiente",
"default": "#2c5282",
"conditional_logic": {
"field": "background_type",
"operator": "==",
"value": "gradient"
},
"description": "Color final del gradiente de fondo"
},
"gradient_angle": {
"type": "number",
"label": "Ángulo del gradiente (grados)",
"default": 135,
"min": 0,
"max": 360,
"conditional_logic": {
"field": "background_type",
"operator": "==",
"value": "gradient"
},
"description": "Ángulo del gradiente en grados (0-360)"
},
"background_image_url": {
"type": "media",
"label": "Imagen de fondo",
"default": "",
"media_type": "image",
"conditional_logic": {
"field": "background_type",
"operator": "==",
"value": "image"
},
"description": "Imagen de fondo para la hero section"
},
"background_overlay": {
"type": "boolean",
"label": "Overlay oscuro sobre imagen",
"default": true,
"conditional_logic": {
"field": "background_type",
"operator": "==",
"value": "image"
},
"description": "Agrega capa oscura sobre la imagen de fondo"
},
"overlay_opacity": {
"type": "number",
"label": "Opacidad del overlay (%)",
"default": 60,
"min": 0,
"max": 100,
"conditional_logic": {
"field": "background_overlay",
"operator": "==",
"value": true
},
"description": "Opacidad de la capa oscura (0-100)"
},
"text_color": {
"type": "color",
"label": "Color del texto",
"default": "#FFFFFF",
"description": "Color del texto del título y elementos"
},
"padding_vertical": {
"type": "select",
"label": "Padding vertical",
"default": "normal",
"options": {
"compact": "Compacto (2rem)",
"normal": "Normal (3rem)",
"spacious": "Espacioso (4rem)",
"extra-spacious": "Extra espacioso (5rem)"
},
"description": "Espaciado vertical de la sección"
},
"margin_bottom": {
"type": "select",
"label": "Margen inferior",
"default": "normal",
"options": {
"none": "Sin margen",
"small": "Pequeño (1rem)",
"normal": "Normal (1.5rem)",
"large": "Grande (2rem)"
},
"description": "Margen inferior de la sección"
},
"category_badge_background": {
"type": "color",
"label": "Fondo de badges",
"default": "rgba(255, 255, 255, 0.2)",
"description": "Color de fondo de los badges de categorías"
},
"category_badge_text_color": {
"type": "color",
"label": "Color del texto de badges",
"default": "#FFFFFF",
"description": "Color del texto en los badges de categorías"
},
"category_badge_blur": {
"type": "boolean",
"label": "Efecto blur en badges",
"default": true,
"description": "Aplica efecto de desenfoque (backdrop-filter) a los badges"
}
}
}
}
}

View File

@@ -1,421 +0,0 @@
{
"component_name": "navbar",
"version": "1.0.0",
"description": "Barra de navegación principal con menú Bootstrap, logo y botón CTA",
"groups": {
"visibility": {
"label": "Visibilidad",
"priority": 10,
"fields": {
"is_enabled": {
"type": "boolean",
"label": "Mostrar navbar",
"default": true,
"required": true,
"description": "Activa o desactiva la barra de navegación"
},
"is_sticky": {
"type": "boolean",
"label": "Navbar fijo (sticky)",
"default": true,
"description": "Mantiene el navbar fijo al hacer scroll"
},
"hide_on_scroll": {
"type": "boolean",
"label": "Ocultar al hacer scroll hacia abajo",
"default": false,
"description": "Oculta el navbar cuando el usuario hace scroll hacia abajo"
},
"show_on_mobile": {
"type": "boolean",
"label": "Mostrar en dispositivos móviles",
"default": true,
"description": "Muestra el navbar en pantallas pequeñas con menú hamburguesa"
}
}
},
"logo": {
"label": "Logo",
"priority": 20,
"fields": {
"logo_type": {
"type": "select",
"label": "Tipo de logo",
"default": "image",
"options": {
"image": "Imagen",
"text": "Texto",
"none": "Sin logo"
},
"required": true,
"description": "Define el tipo de logo a mostrar"
},
"logo_image_url": {
"type": "media",
"label": "Imagen del logo",
"default": "",
"media_type": "image",
"conditional_logic": {
"field": "logo_type",
"operator": "==",
"value": "image"
},
"required": true,
"description": "Sube la imagen del logo (recomendado: PNG transparente)"
},
"logo_image_width": {
"type": "number",
"label": "Ancho del logo (px)",
"default": 150,
"min": 50,
"max": 400,
"conditional_logic": {
"field": "logo_type",
"operator": "==",
"value": "image"
},
"description": "Ancho del logo en píxeles"
},
"logo_text": {
"type": "text",
"label": "Texto del logo",
"default": "",
"maxlength": 50,
"conditional_logic": {
"field": "logo_type",
"operator": "==",
"value": "text"
},
"description": "Texto a mostrar como logo"
},
"logo_link": {
"type": "url",
"label": "Enlace del logo",
"default": "",
"placeholder": "Dejar vacío para usar la URL del home",
"description": "URL de destino al hacer clic en el logo"
},
"logo_position": {
"type": "select",
"label": "Posición del logo",
"default": "left",
"options": {
"left": "Izquierda",
"center": "Centro",
"right": "Derecha"
},
"description": "Posición del logo en el navbar"
}
}
},
"menu": {
"label": "Menú de Navegación",
"priority": 30,
"fields": {
"menu_location": {
"type": "select",
"label": "Ubicación del menú",
"default": "primary",
"options": {
"primary": "Menú Principal",
"secondary": "Menú Secundario",
"custom": "Menú personalizado"
},
"required": true,
"description": "Selecciona qué menú de WordPress mostrar"
},
"custom_menu_id": {
"type": "number",
"label": "ID del menú personalizado",
"default": 0,
"min": 0,
"conditional_logic": {
"field": "menu_location",
"operator": "==",
"value": "custom"
},
"description": "ID del menú personalizado de WordPress"
},
"menu_alignment": {
"type": "select",
"label": "Alineación del menú",
"default": "left",
"options": {
"left": "Izquierda",
"center": "Centro",
"right": "Derecha"
},
"description": "Alineación de los items del menú"
},
"enable_dropdowns": {
"type": "boolean",
"label": "Habilitar menús desplegables",
"default": true,
"description": "Permite submenús desplegables"
},
"dropdown_animation": {
"type": "select",
"label": "Animación de dropdowns",
"default": "fade",
"options": {
"none": "Sin animación",
"fade": "Aparecer gradualmente",
"slide": "Deslizar"
},
"conditional_logic": {
"field": "enable_dropdowns",
"operator": "==",
"value": true
},
"description": "Tipo de animación para los submenús"
},
"mobile_breakpoint": {
"type": "select",
"label": "Breakpoint para menú móvil",
"default": "lg",
"options": {
"sm": "Small (576px)",
"md": "Medium (768px)",
"lg": "Large (992px)",
"xl": "Extra Large (1200px)"
},
"description": "Punto de quiebre para mostrar menú hamburguesa"
}
}
},
"cta_button": {
"label": "Botón CTA (Call to Action)",
"priority": 40,
"fields": {
"button_enabled": {
"type": "boolean",
"label": "Mostrar botón CTA",
"default": true,
"description": "Activa o desactiva el botón de llamada a la acción"
},
"button_text": {
"type": "text",
"label": "Texto del botón",
"default": "Let's Talk",
"maxlength": 30,
"conditional_logic": {
"field": "button_enabled",
"operator": "==",
"value": true
},
"required": true,
"description": "Texto que aparece en el botón"
},
"button_icon": {
"type": "text",
"label": "Ícono del botón",
"default": "bi-lightning-charge-fill",
"placeholder": "Ej: bi-lightning-charge-fill",
"conditional_logic": {
"field": "button_enabled",
"operator": "==",
"value": true
},
"description": "Clase del ícono Bootstrap (dejar vacío para sin ícono)"
},
"button_action_type": {
"type": "select",
"label": "Tipo de acción del botón",
"default": "modal",
"options": {
"modal": "Abrir modal",
"link": "Ir a URL",
"scroll": "Scroll a sección"
},
"conditional_logic": {
"field": "button_enabled",
"operator": "==",
"value": true
},
"required": true,
"description": "Acción que se ejecuta al hacer clic"
},
"button_modal_target": {
"type": "text",
"label": "ID del modal",
"default": "#contactModal",
"placeholder": "#contactModal",
"conditional_logic": {
"field": "button_action_type",
"operator": "==",
"value": "modal"
},
"required": true,
"description": "ID del modal de Bootstrap a abrir (incluir #)"
},
"button_link_url": {
"type": "url",
"label": "URL del enlace",
"default": "",
"placeholder": "https://",
"conditional_logic": {
"field": "button_action_type",
"operator": "==",
"value": "link"
},
"required": true,
"description": "URL de destino del botón"
},
"button_link_target": {
"type": "select",
"label": "Abrir enlace en",
"default": "_self",
"options": {
"_self": "Misma ventana",
"_blank": "Nueva ventana"
},
"conditional_logic": {
"field": "button_action_type",
"operator": "==",
"value": "link"
},
"description": "Destino del enlace"
},
"button_scroll_target": {
"type": "text",
"label": "ID de la sección",
"default": "",
"placeholder": "#contact",
"conditional_logic": {
"field": "button_action_type",
"operator": "==",
"value": "scroll"
},
"required": true,
"description": "ID de la sección a la que hacer scroll (incluir #)"
},
"button_position": {
"type": "select",
"label": "Posición del botón",
"default": "right",
"options": {
"left": "Antes del menú",
"right": "Después del menú"
},
"conditional_logic": {
"field": "button_enabled",
"operator": "==",
"value": true
},
"description": "Ubicación del botón en el navbar"
}
}
},
"styles": {
"label": "Estilos",
"priority": 50,
"fields": {
"background_color": {
"type": "color",
"label": "Color de fondo",
"default": "#1e3a5f",
"description": "Color de fondo del navbar (por defecto: navy primary)"
},
"text_color": {
"type": "color",
"label": "Color del texto",
"default": "#FFFFFF",
"description": "Color del texto de los links del menú"
},
"hover_color": {
"type": "color",
"label": "Color hover",
"default": "#FF8600",
"description": "Color al pasar el mouse sobre los links (por defecto: orange primary)"
},
"active_color": {
"type": "color",
"label": "Color del item activo",
"default": "#FF8600",
"description": "Color del item de menú activo/actual"
},
"button_background": {
"type": "color",
"label": "Color de fondo del botón",
"default": "#FF8600",
"description": "Color de fondo del botón CTA"
},
"button_text_color": {
"type": "color",
"label": "Color del texto del botón",
"default": "#FFFFFF",
"description": "Color del texto del botón CTA"
},
"button_hover_background": {
"type": "color",
"label": "Color hover del botón",
"default": "#FF6B35",
"description": "Color de fondo del botón al hacer hover"
},
"padding_vertical": {
"type": "select",
"label": "Padding vertical",
"default": "normal",
"options": {
"compact": "Compacto (0.5rem)",
"normal": "Normal (1rem)",
"spacious": "Espacioso (1.5rem)"
},
"description": "Espaciado vertical del navbar"
},
"shadow_enabled": {
"type": "boolean",
"label": "Activar sombra",
"default": true,
"description": "Agrega sombra debajo del navbar"
},
"shadow_intensity": {
"type": "select",
"label": "Intensidad de la sombra",
"default": "medium",
"options": {
"light": "Ligera",
"medium": "Media",
"strong": "Fuerte"
},
"conditional_logic": {
"field": "shadow_enabled",
"operator": "==",
"value": true
},
"description": "Intensidad de la sombra del navbar"
},
"border_bottom_enabled": {
"type": "boolean",
"label": "Borde inferior",
"default": false,
"description": "Agrega un borde en la parte inferior del navbar"
},
"border_bottom_color": {
"type": "color",
"label": "Color del borde inferior",
"default": "#FF8600",
"conditional_logic": {
"field": "border_bottom_enabled",
"operator": "==",
"value": true
},
"description": "Color del borde inferior"
},
"border_bottom_width": {
"type": "number",
"label": "Grosor del borde (px)",
"default": 3,
"min": 1,
"max": 10,
"conditional_logic": {
"field": "border_bottom_enabled",
"operator": "==",
"value": true
},
"description": "Grosor del borde inferior en píxeles"
}
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More