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

0
Admin/.gitkeep Normal file
View File

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>