diff --git a/src/Component/Application/DTO/.gitkeep b/Admin/.gitkeep
similarity index 100%
rename from src/Component/Application/DTO/.gitkeep
rename to Admin/.gitkeep
diff --git a/Admin/Application/UseCases/RenderDashboardUseCase.php b/Admin/Application/UseCases/RenderDashboardUseCase.php
new file mode 100644
index 00000000..fec459c3
--- /dev/null
+++ b/Admin/Application/UseCases/RenderDashboardUseCase.php
@@ -0,0 +1,41 @@
+renderer->supports($viewType)) {
+ throw new \RuntimeException(
+ sprintf('Renderer does not support view type: %s', $viewType)
+ );
+ }
+
+ return $this->renderer->render();
+ }
+}
diff --git a/Admin/ContactForm/Infrastructure/Ui/ContactFormFormBuilder.php b/Admin/ContactForm/Infrastructure/Ui/ContactFormFormBuilder.php
new file mode 100644
index 00000000..f0c3cc5b
--- /dev/null
+++ b/Admin/ContactForm/Infrastructure/Ui/ContactFormFormBuilder.php
@@ -0,0 +1,601 @@
+buildHeader($componentId);
+
+ $html .= '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Integracion Webhook';
+ $html .= ' Privado ';
+ $html .= ' ';
+
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' El webhook URL nunca se expone en el frontend. Los datos se envian de forma segura desde el servidor.';
+ $html .= '
';
+
+ $webhookUrl = $this->renderer->getFieldValue($componentId, 'integration', 'webhook_url', '');
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' URL del Webhook';
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildMessagesGroup(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Mensajes';
+ $html .= ' ';
+
+ $successMessage = $this->renderer->getFieldValue($componentId, 'messages', 'success_message', '¡Gracias por contactarnos! Te responderemos pronto.');
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Mensaje de exito';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= '
';
+
+ $errorMessage = $this->renderer->getFieldValue($componentId, 'messages', 'error_message', 'Hubo un error al enviar el mensaje. Por favor intenta de nuevo.');
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Mensaje de error';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= '
';
+
+ $sendingMessage = $this->renderer->getFieldValue($componentId, 'messages', 'sending_message', 'Enviando...');
+ $html .= '
';
+ $html .= ' Mensaje enviando ';
+ $html .= ' ';
+ $html .= '
';
+
+ $validationRequired = $this->renderer->getFieldValue($componentId, 'messages', 'validation_required', 'Este campo es obligatorio');
+ $html .= '
';
+ $html .= ' Error campo requerido ';
+ $html .= ' ';
+ $html .= '
';
+
+ $validationEmail = $this->renderer->getFieldValue($componentId, 'messages', 'validation_email', 'Por favor ingresa un email válido');
+ $html .= '
';
+ $html .= ' Error email invalido ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildColorsGroup(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Colores';
+ $html .= ' ';
+
+ // Seccion
+ $html .= '
Seccion
';
+ $html .= '
';
+
+ $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 .= '
';
+
+ // Boton
+ $html .= '
Boton
';
+ $html .= '
';
+
+ $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 .= '
';
+
+ $html .= '
';
+
+ $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 .= '
';
+
+ // Mensajes
+ $html .= '
Mensajes
';
+ $html .= '
';
+
+ $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 .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildSpacingGroup(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Espaciado';
+ $html .= ' ';
+
+ $html .= '
';
+
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildEffectsGroup(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Efectos Visuales';
+ $html .= ' ';
+
+ $html .= '
';
+
+ $html .= '
';
+
+ $textareaRows = $this->renderer->getFieldValue($componentId, 'visual_effects', 'textarea_rows', '4');
+ $html .= '
';
+ $html .= ' Filas textarea ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildSwitch(string $id, string $label, string $icon, mixed $checked): string
+ {
+ $checked = $checked === true || $checked === '1' || $checked === 1;
+
+ $html = '
';
+ $html .= '
';
+ $html .= sprintf(
+ ' ',
+ esc_attr($id),
+ $checked ? 'checked' : ''
+ );
+ $html .= sprintf(
+ ' ',
+ esc_attr($id)
+ );
+ $html .= sprintf(' ', esc_attr($icon));
+ $html .= sprintf(' %s ', esc_html($label));
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
';
+
+ 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 = '
';
+
+ return $html;
+ }
+}
diff --git a/Admin/CtaBoxSidebar/Infrastructure/Ui/CtaBoxSidebarFormBuilder.php b/Admin/CtaBoxSidebar/Infrastructure/Ui/CtaBoxSidebarFormBuilder.php
new file mode 100644
index 00000000..eea6b8cc
--- /dev/null
+++ b/Admin/CtaBoxSidebar/Infrastructure/Ui/CtaBoxSidebarFormBuilder.php
@@ -0,0 +1,518 @@
+buildHeader($componentId);
+
+ $html .= '
';
+
+ // Columna izquierda
+ $html .= '
';
+ $html .= $this->buildVisibilityGroup($componentId);
+ $html .= $this->buildContentGroup($componentId);
+ $html .= $this->buildBehaviorGroup($componentId);
+ $html .= '
';
+
+ // Columna derecha
+ $html .= '
';
+ $html .= $this->buildTypographyGroup($componentId);
+ $html .= $this->buildColorsGroup($componentId);
+ $html .= $this->buildSpacingGroup($componentId);
+ $html .= $this->buildEffectsGroup($componentId);
+ $html .= '
';
+
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildHeader(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Visibilidad';
+ $html .= ' ';
+
+ // 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 .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Mostrar en';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Todos ';
+ $html .= ' Solo posts ';
+ $html .= ' Solo paginas ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildContentGroup(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Contenido';
+ $html .= ' ';
+
+ // title
+ $title = $this->renderer->getFieldValue($componentId, 'content', 'title', '¿Listo para potenciar tus proyectos?');
+ $html .= '
';
+ $html .= ' Titulo ';
+ $html .= ' ';
+ $html .= '
';
+
+ // description
+ $description = $this->renderer->getFieldValue($componentId, 'content', 'description', 'Accede a nuestra biblioteca completa de APUs y herramientas profesionales.');
+ $html .= '
';
+ $html .= ' Descripcion ';
+ $html .= ' ';
+ $html .= '
';
+
+ // button_text
+ $buttonText = $this->renderer->getFieldValue($componentId, 'content', 'button_text', 'Solicitar Demo');
+ $html .= '
';
+ $html .= ' Texto del boton ';
+ $html .= ' ';
+ $html .= '
';
+
+ // button_icon
+ $buttonIcon = $this->renderer->getFieldValue($componentId, 'content', 'button_icon', 'bi bi-calendar-check');
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Icono del boton';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Clase de Bootstrap Icons ';
+ $html .= '
';
+
+ // button_action
+ $buttonAction = $this->renderer->getFieldValue($componentId, 'content', 'button_action', 'modal');
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Accion del boton';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Abrir modal ';
+ $html .= ' Ir a URL ';
+ $html .= ' Scroll a seccion ';
+ $html .= ' ';
+ $html .= '
';
+
+ // button_link
+ $buttonLink = $this->renderer->getFieldValue($componentId, 'content', 'button_link', '#contactModal');
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' URL/ID destino';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Para modal usa #nombreModal, para scroll usa #idSeccion ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildBehaviorGroup(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Comportamiento';
+ $html .= ' ';
+
+ // text_align
+ $textAlign = $this->renderer->getFieldValue($componentId, 'behavior', 'text_align', 'center');
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Alineacion del texto';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Izquierda ';
+ $html .= ' Centro ';
+ $html .= ' Derecha ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildTypographyGroup(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Tipografia';
+ $html .= ' ';
+
+ $html .= '
';
+
+ $html .= '
';
+
+ $html .= '
';
+
+ // button_font_weight
+ $buttonFontWeight = $this->renderer->getFieldValue($componentId, 'typography', 'button_font_weight', '700');
+ $html .= '
';
+ $html .= ' Peso boton ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildColorsGroup(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Colores';
+ $html .= ' ';
+
+ // Colores principales
+ $html .= '
Contenedor
';
+ $html .= '
';
+
+ $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 .= '
';
+
+ $html .= '
';
+
+ $descColor = $this->renderer->getFieldValue($componentId, 'colors', 'description_color', 'rgba(255, 255, 255, 0.95)');
+ $html .= $this->buildColorPicker('ctaDescriptionColor', 'Descripcion', $descColor);
+
+ $html .= '
';
+
+ // Colores del boton
+ $html .= '
Boton
';
+ $html .= '
';
+
+ $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 .= '
';
+
+ // Colores hover
+ $html .= '
Boton Hover
';
+ $html .= '
';
+
+ $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 .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildSpacingGroup(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Espaciado';
+ $html .= ' ';
+
+ $html .= '
';
+
+ $html .= '
';
+
+ $html .= '
';
+
+ // icon_margin_right
+ $iconMarginRight = $this->renderer->getFieldValue($componentId, 'spacing', 'icon_margin_right', '0.5rem');
+ $html .= '
';
+ $html .= ' Margen icono ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildEffectsGroup(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Efectos Visuales';
+ $html .= ' ';
+
+ $html .= '
';
+
+ $html .= '
';
+
+ // box_shadow
+ $boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', '0 4px 12px rgba(255, 133, 0, 0.2)');
+ $html .= '
';
+ $html .= ' Sombra ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+
+ $html .= '
';
+
+ // transition_duration
+ $transitionDuration = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s');
+ $html .= '
';
+ $html .= ' Duracion transicion ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildSwitch(string $id, string $label, string $icon, bool $checked): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= sprintf(
+ ' ',
+ esc_attr($id),
+ $checked ? 'checked' : ''
+ );
+ $html .= sprintf(
+ ' ',
+ esc_attr($id)
+ );
+ $html .= sprintf(' ', esc_attr($icon));
+ $html .= sprintf(' %s ', esc_html($label));
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildColorPicker(string $id, string $label, string $value): string
+ {
+ $html = '
';
+ $html .= sprintf(
+ '
%s ',
+ esc_html($label)
+ );
+ $html .= '
';
+ $html .= sprintf(
+ ' ',
+ esc_attr($id),
+ esc_attr($value)
+ );
+ $html .= sprintf(
+ ' %s ',
+ esc_attr($id),
+ esc_html(strtoupper($value))
+ );
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+}
diff --git a/Admin/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkFormBuilder.php b/Admin/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkFormBuilder.php
new file mode 100644
index 00000000..a1e76b00
--- /dev/null
+++ b/Admin/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkFormBuilder.php
@@ -0,0 +1,450 @@
+buildHeader($componentId);
+
+ // Layout 2 columnas
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildVisibilityGroup($componentId);
+ $html .= $this->buildContentGroup($componentId);
+ $html .= $this->buildBehaviorGroup($componentId);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildTypographyGroup($componentId);
+ $html .= $this->buildColorsGroup($componentId);
+ $html .= $this->buildSpacingGroup($componentId);
+ $html .= $this->buildVisualEffectsGroup($componentId);
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildHeader(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Visibilidad';
+ $html .= ' ';
+
+ // Switch: Enabled
+ $enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Mostrar botón Let\'s Talk ';
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
';
+
+ // Switch: Show on Desktop
+ $showDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Mostrar en escritorio (≥992px) ';
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
';
+
+ // Switch: Show on Mobile
+ $showMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', false);
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Mostrar en móvil (<992px) ';
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
';
+
+ // Select: Show on Pages
+ $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all');
+ $html .= '
';
+ $html .= ' Mostrar en ';
+ $html .= ' ';
+ $html .= ' Todas las páginas ';
+ $html .= ' Solo página de inicio ';
+ $html .= ' Solo posts individuales ';
+ $html .= ' Solo páginas ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildContentGroup(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Contenido';
+ $html .= ' ';
+
+ // Text: Button Text
+ $buttonText = $this->renderer->getFieldValue($componentId, 'content', 'button_text', "Let's Talk");
+ $html .= '
';
+ $html .= ' Texto del botón ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Switch: Show Icon
+ $showIcon = $this->renderer->getFieldValue($componentId, 'content', 'show_icon', true);
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Mostrar ícono ';
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
';
+
+ // Text: Icon Class
+ $iconClass = $this->renderer->getFieldValue($componentId, 'content', 'icon_class', 'bi-lightning-charge-fill');
+ $html .= '
';
+ $html .= '
';
+ $html .= ' Clase del ícono ';
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
Usa clases de Bootstrap Icons (ej: bi-chat-dots) ';
+ $html .= '
';
+
+ // Text: Modal Target
+ $modalTarget = $this->renderer->getFieldValue($componentId, 'content', 'modal_target', '#contactModal');
+ $html .= '
';
+ $html .= ' ID del modal ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Text: ARIA Label
+ $ariaLabel = $this->renderer->getFieldValue($componentId, 'content', 'aria_label', 'Abrir formulario de contacto');
+ $html .= '
';
+ $html .= ' Etiqueta ARIA (accesibilidad) ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildBehaviorGroup(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Comportamiento';
+ $html .= ' ';
+
+ // Switch: Enable Modal
+ $enableModal = $this->renderer->getFieldValue($componentId, 'behavior', 'enable_modal', true);
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Abrir modal al hacer clic ';
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
Si está desactivado, usará la URL personalizada ';
+ $html .= '
';
+
+ // URL: Custom URL
+ $customUrl = $this->renderer->getFieldValue($componentId, 'behavior', 'custom_url', '');
+ $html .= '
';
+ $html .= ' URL personalizada ';
+ $html .= ' ';
+ $html .= ' Solo se usa si "Abrir modal" está desactivado ';
+ $html .= '
';
+
+ // Switch: Open in New Tab
+ $openNewTab = $this->renderer->getFieldValue($componentId, 'behavior', 'open_in_new_tab', false);
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Abrir en nueva pestaña ';
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildTypographyGroup(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Tipografía';
+ $html .= ' ';
+
+ // Text: Font Size
+ $fontSize = $this->renderer->getFieldValue($componentId, 'typography', 'font_size', '1rem');
+ $html .= '
';
+ $html .= ' Tamaño de fuente ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Select: Font Weight
+ $fontWeight = $this->renderer->getFieldValue($componentId, 'typography', 'font_weight', '600');
+ $html .= '
';
+ $html .= ' Peso de fuente ';
+ $html .= ' ';
+ $html .= ' Normal (400) ';
+ $html .= ' Medium (500) ';
+ $html .= ' Semibold (600) ';
+ $html .= ' Bold (700) ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Select: Text Transform
+ $textTransform = $this->renderer->getFieldValue($componentId, 'typography', 'text_transform', 'none');
+ $html .= '
';
+ $html .= ' Transformación de texto ';
+ $html .= ' ';
+ $html .= ' Normal ';
+ $html .= ' MAYÚSCULAS ';
+ $html .= ' minúsculas ';
+ $html .= ' Capitalizado ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildColorsGroup(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Colores';
+ $html .= ' ';
+
+ // Color: Background
+ $bgColor = $this->renderer->getFieldValue($componentId, 'colors', 'background_color', '#FF8600');
+ $html .= '
';
+ $html .= ' Color de fondo ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Color: Background Hover
+ $bgHoverColor = $this->renderer->getFieldValue($componentId, 'colors', 'background_hover_color', '#FF6B35');
+ $html .= '
';
+ $html .= ' Color de fondo (hover) ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Color: Text
+ $textColor = $this->renderer->getFieldValue($componentId, 'colors', 'text_color', '#FFFFFF');
+ $html .= '
';
+ $html .= ' Color del texto ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Color: Text Hover
+ $textHoverColor = $this->renderer->getFieldValue($componentId, 'colors', 'text_hover_color', '#FFFFFF');
+ $html .= '
';
+ $html .= ' Color del texto (hover) ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Text: Border Color (permite transparent)
+ $borderColor = $this->renderer->getFieldValue($componentId, 'colors', 'border_color', 'transparent');
+ $html .= '
';
+ $html .= ' Color del borde ';
+ $html .= ' ';
+ $html .= ' Usa "transparent" para sin borde visible ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildSpacingGroup(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Espaciado';
+ $html .= ' ';
+
+ // Text: Padding Top/Bottom
+ $paddingTB = $this->renderer->getFieldValue($componentId, 'spacing', 'padding_top_bottom', '0.5rem');
+ $html .= '
';
+ $html .= ' Padding vertical ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Text: Padding Left/Right
+ $paddingLR = $this->renderer->getFieldValue($componentId, 'spacing', 'padding_left_right', '1.5rem');
+ $html .= '
';
+ $html .= ' Padding horizontal ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Text: Margin Left
+ $marginLeft = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_left', '1rem');
+ $html .= '
';
+ $html .= ' Margen izquierdo (desktop) ';
+ $html .= ' ';
+ $html .= ' Separación del menú en pantallas ≥992px ';
+ $html .= '
';
+
+ // Text: Icon Spacing
+ $iconSpacing = $this->renderer->getFieldValue($componentId, 'spacing', 'icon_spacing', '0.5rem');
+ $html .= '
';
+ $html .= ' Espaciado del ícono ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildVisualEffectsGroup(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Efectos Visuales';
+ $html .= ' ';
+
+ // Text: Border Radius
+ $borderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_radius', '6px');
+ $html .= '
';
+ $html .= ' Radio de bordes ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Text: Border Width
+ $borderWidth = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_width', '0');
+ $html .= '
';
+ $html .= ' Grosor del borde ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Text: Box Shadow
+ $boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', 'none');
+ $html .= '
';
+ $html .= ' Sombra ';
+ $html .= ' ';
+ $html .= ' Ej: 0 2px 4px rgba(0,0,0,0.1) ';
+ $html .= '
';
+
+ // Text: Transition Duration
+ $transitionDuration = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s');
+ $html .= '
';
+ $html .= ' Duración de transición ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+}
diff --git a/Admin/CtaPost/Infrastructure/Ui/CtaPostFormBuilder.php b/Admin/CtaPost/Infrastructure/Ui/CtaPostFormBuilder.php
new file mode 100644
index 00000000..ee1e14f4
--- /dev/null
+++ b/Admin/CtaPost/Infrastructure/Ui/CtaPostFormBuilder.php
@@ -0,0 +1,440 @@
+buildHeader($componentId);
+
+ $html .= '
';
+
+ // Columna izquierda
+ $html .= '
';
+ $html .= $this->buildVisibilityGroup($componentId);
+ $html .= $this->buildContentGroup($componentId);
+ $html .= $this->buildTypographyGroup($componentId);
+ $html .= '
';
+
+ // Columna derecha
+ $html .= '
';
+ $html .= $this->buildColorsGroup($componentId);
+ $html .= $this->buildSpacingGroup($componentId);
+ $html .= $this->buildEffectsGroup($componentId);
+ $html .= '
';
+
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildHeader(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Visibilidad';
+ $html .= ' ';
+
+ $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 .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Mostrar en';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Todos ';
+ $html .= ' Solo posts ';
+ $html .= ' Solo paginas ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildContentGroup(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Contenido';
+ $html .= ' ';
+
+ // Title
+ $title = $this->renderer->getFieldValue($componentId, 'content', 'title', 'Accede a 200,000+ Analisis de Precios Unitarios');
+ $html .= '
';
+ $html .= ' Titulo ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Description
+ $description = $this->renderer->getFieldValue($componentId, 'content', 'description', '');
+ $html .= '
';
+ $html .= ' Descripcion ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Button Text
+ $buttonText = $this->renderer->getFieldValue($componentId, 'content', 'button_text', 'Ver Catalogo Completo');
+ $html .= '
';
+ $html .= ' Texto del boton ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Button URL
+ $buttonUrl = $this->renderer->getFieldValue($componentId, 'content', 'button_url', '/catalogo');
+ $html .= '
';
+ $html .= ' URL del boton ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Button Icon
+ $buttonIcon = $this->renderer->getFieldValue($componentId, 'content', 'button_icon', 'bi-arrow-right');
+ $html .= '
';
+ $html .= ' Icono del boton ';
+ $html .= ' ';
+ $html .= ' Clase de Bootstrap Icons ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildTypographyGroup(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Tipografia';
+ $html .= ' ';
+
+ $html .= '
';
+
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildColorsGroup(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Colores';
+ $html .= ' ';
+
+ // Gradiente
+ $html .= '
Gradiente de fondo
';
+ $html .= '
';
+
+ $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 .= '
';
+
+ // Textos
+ $html .= '
Textos
';
+ $html .= '
';
+
+ $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 .= '
';
+
+ // Boton
+ $html .= '
Boton
';
+ $html .= '
';
+
+ $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 .= '
';
+
+ $html .= '
';
+
+ $buttonHoverBg = $this->renderer->getFieldValue($componentId, 'colors', 'button_hover_bg', '#f8f9fa');
+ $html .= $this->buildColorPicker('ctaPostButtonHoverBg', 'Hover', $buttonHoverBg);
+
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildSpacingGroup(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Espaciado';
+ $html .= ' ';
+
+ $html .= '
';
+
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildEffectsGroup(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Efectos Visuales';
+ $html .= ' ';
+
+ $html .= '
';
+
+ $html .= '
';
+
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildSwitch(string $id, string $label, string $icon, mixed $checked): string
+ {
+ $checked = $checked === true || $checked === '1' || $checked === 1;
+
+ $html = '
';
+ $html .= '
';
+ $html .= sprintf(
+ ' ',
+ esc_attr($id),
+ $checked ? 'checked' : ''
+ );
+ $html .= sprintf(
+ ' ',
+ esc_attr($id)
+ );
+ $html .= sprintf(' ', esc_attr($icon));
+ $html .= sprintf(' %s ', esc_html($label));
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildColorPicker(string $id, string $label, string $value): string
+ {
+ $html = '
';
+ $html .= sprintf(
+ '
%s ',
+ esc_html($label)
+ );
+ $html .= '
';
+ $html .= sprintf(
+ ' ',
+ esc_attr($id),
+ esc_attr($value)
+ );
+ $html .= sprintf(
+ ' %s ',
+ esc_attr($id),
+ esc_html(strtoupper($value))
+ );
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+}
diff --git a/Admin/Domain/Contracts/ComponentTabInterface.php b/Admin/Domain/Contracts/ComponentTabInterface.php
new file mode 100644
index 00000000..dd65cb27
--- /dev/null
+++ b/Admin/Domain/Contracts/ComponentTabInterface.php
@@ -0,0 +1,48 @@
+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;
+ }
+}
diff --git a/Admin/FeaturedImage/Infrastructure/Ui/FeaturedImageFormBuilder.php b/Admin/FeaturedImage/Infrastructure/Ui/FeaturedImageFormBuilder.php
new file mode 100644
index 00000000..fbdf247e
--- /dev/null
+++ b/Admin/FeaturedImage/Infrastructure/Ui/FeaturedImageFormBuilder.php
@@ -0,0 +1,280 @@
+buildHeader($componentId);
+
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildVisibilityGroup($componentId);
+ $html .= $this->buildContentGroup($componentId);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildSpacingGroup($componentId);
+ $html .= $this->buildEffectsGroup($componentId);
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildHeader(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Visibilidad';
+ $html .= ' ';
+
+ $enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Mostrar imagen destacada ';
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
';
+
+ $showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Mostrar en Desktop ';
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
';
+
+ $showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Mostrar en Mobile ';
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
';
+
+ $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Mostrar en';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Todas las paginas ';
+ $html .= ' Solo posts individuales ';
+ $html .= ' Solo paginas ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildContentGroup(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Contenido';
+ $html .= ' ';
+
+ $imageSize = $this->renderer->getFieldValue($componentId, 'content', 'image_size', 'roi-featured-large');
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Tamano de imagen';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Grande (1200x600) ';
+ $html .= ' Mediano (800x400) ';
+ $html .= ' Original (tamano completo) ';
+ $html .= ' ';
+ $html .= '
';
+
+ $lazyLoading = $this->renderer->getFieldValue($componentId, 'content', 'lazy_loading', true);
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Carga diferida (lazy loading) ';
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
Mejora rendimiento cargando imagen cuando es visible ';
+ $html .= '
';
+
+ $linkToMedia = $this->renderer->getFieldValue($componentId, 'content', 'link_to_media', false);
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Enlazar a imagen completa ';
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
Abre la imagen en tamano completo al hacer clic ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildSpacingGroup(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Espaciado';
+ $html .= ' ';
+
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildEffectsGroup(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Efectos Visuales';
+ $html .= ' ';
+
+ $borderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_radius', '12px');
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Radio de bordes';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= '
';
+
+ $boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', '0 8px 24px rgba(0, 0, 0, 0.1)');
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Sombra';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= '
';
+
+ $hoverEffect = $this->renderer->getFieldValue($componentId, 'visual_effects', 'hover_effect', true);
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Efecto hover ';
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
Aplica efecto de escala sutil al pasar el mouse ';
+ $html .= '
';
+
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+}
diff --git a/Admin/Footer/Infrastructure/Ui/FooterFormBuilder.php b/Admin/Footer/Infrastructure/Ui/FooterFormBuilder.php
new file mode 100644
index 00000000..fe9f3418
--- /dev/null
+++ b/Admin/Footer/Infrastructure/Ui/FooterFormBuilder.php
@@ -0,0 +1,413 @@
+buildHeader($componentId);
+
+ $html .= '
';
+
+ // Columna izquierda
+ $html .= '
';
+ $html .= $this->buildVisibilityGroup($componentId);
+ $html .= $this->buildWidget1Group($componentId);
+ $html .= $this->buildWidget2Group($componentId);
+ $html .= $this->buildWidget3Group($componentId);
+ $html .= $this->buildNewsletterGroup($componentId);
+ $html .= '
';
+
+ // Columna derecha
+ $html .= '
';
+ $html .= $this->buildFooterBottomGroup($componentId);
+ $html .= $this->buildColorsGroup($componentId);
+ $html .= $this->buildSpacingGroup($componentId);
+ $html .= $this->buildEffectsGroup($componentId);
+ $html .= '
';
+
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildHeader(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Visibilidad';
+ $html .= ' ';
+
+ $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 .= ' ';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildWidget1Group(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Widget 1 (Menu)';
+ $html .= ' ';
+
+ $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 .= '
';
+ $html .= ' ';
+ $html .= ' El contenido se gestiona desde Apariencia > Menus > Footer Menu 1 ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildWidget2Group(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Widget 2 (Menu)';
+ $html .= ' ';
+
+ $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 .= '
';
+ $html .= ' ';
+ $html .= ' El contenido se gestiona desde Apariencia > Menus > Footer Menu 2 ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildWidget3Group(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Widget 3 (Menu)';
+ $html .= ' ';
+
+ $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 .= '
';
+ $html .= ' ';
+ $html .= ' El contenido se gestiona desde Apariencia > Menus > Footer Menu 3 ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildNewsletterGroup(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Newsletter';
+ $html .= ' ';
+
+ $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 .= ' ';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildFooterBottomGroup(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Pie de Footer';
+ $html .= ' ';
+
+ $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 .= '
';
+ $html .= ' ';
+ $html .= ' El simbolo © se agrega automaticamente';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildColorsGroup(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Colores';
+ $html .= ' ';
+
+ $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 .= ' ';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildSpacingGroup(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Espaciado';
+ $html .= ' ';
+
+ $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 .= ' ';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildEffectsGroup(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Efectos Visuales';
+ $html .= ' ';
+
+ $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 .= ' ';
+ $html .= '
';
+
+ return $html;
+ }
+
+ // Helper methods
+ private function buildSwitch(string $id, string $label, string $icon, $value): string
+ {
+ $checked = $value === true || $value === '1' || $value === 1 ? 'checked' : '';
+
+ $html = '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' ' . esc_html($label);
+ $html .= ' ';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildTextInput(string $id, string $label, string $icon, mixed $value): string
+ {
+ $value = $this->normalizeStringValue($value);
+
+ $html = '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' ' . esc_html($label);
+ $html .= ' ';
+ $html .= ' ';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildPasswordInput(string $id, string $label, string $icon, mixed $value): string
+ {
+ $value = $this->normalizeStringValue($value);
+
+ $html = '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ' . esc_html($label);
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
URL oculta por seguridad
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildTextarea(string $id, string $label, string $icon, mixed $value): string
+ {
+ $value = $this->normalizeStringValue($value);
+
+ $html = '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' ' . esc_html($label);
+ $html .= ' ';
+ $html .= ' ';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildColorInput(string $id, string $label, mixed $value): string
+ {
+ $value = $this->normalizeStringValue($value);
+
+ $html = '
';
+ $html .= ' ';
+ $html .= ' ' . esc_html($label) . ' ';
+ $html .= '
';
+
+ 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;
+ }
+}
diff --git a/Admin/Hero/Infrastructure/Ui/HeroFormBuilder.php b/Admin/Hero/Infrastructure/Ui/HeroFormBuilder.php
new file mode 100644
index 00000000..160dce70
--- /dev/null
+++ b/Admin/Hero/Infrastructure/Ui/HeroFormBuilder.php
@@ -0,0 +1,416 @@
+buildHeader($componentId);
+
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildVisibilityGroup($componentId);
+ $html .= $this->buildContentGroup($componentId);
+ $html .= $this->buildEffectsGroup($componentId);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildColorsGroup($componentId);
+ $html .= $this->buildTypographyGroup($componentId);
+ $html .= $this->buildSpacingGroup($componentId);
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildHeader(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Visibilidad';
+ $html .= ' ';
+
+ $enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Activar Hero Section ';
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
';
+
+ $showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Mostrar en Desktop ';
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
';
+
+ $showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Mostrar en Mobile ';
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
';
+
+ $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Mostrar en';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Todas las páginas ';
+ $html .= ' Solo posts individuales ';
+ $html .= ' Solo páginas ';
+ $html .= ' Solo página de inicio ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildContentGroup(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Contenido';
+ $html .= ' ';
+
+ $showCategories = $this->renderer->getFieldValue($componentId, 'content', 'show_categories', true);
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Mostrar badges de categorías ';
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
';
+
+ $showBadgeIcon = $this->renderer->getFieldValue($componentId, 'content', 'show_badge_icon', true);
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Mostrar ícono en badges ';
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
';
+
+ $badgeIconClass = $this->renderer->getFieldValue($componentId, 'content', 'badge_icon_class', 'bi-folder-fill');
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Clase del ícono de badge';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Usa clases de Bootstrap Icons ';
+ $html .= '
';
+
+ $titleTag = $this->renderer->getFieldValue($componentId, 'content', 'title_tag', 'h1');
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Etiqueta HTML del título';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' H1 (recomendado para SEO) ';
+ $html .= ' H2 ';
+ $html .= ' DIV (sin semántica) ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildColorsGroup(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Colores';
+ $html .= ' ';
+
+ $html .= '
';
+
+ $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 .= '
';
+ $html .= '
';
+
+ $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 .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildTypographyGroup(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Tipografía';
+ $html .= ' ';
+
+ $html .= '
';
+ $html .= '
';
+
+ $titleFontWeight = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_weight', '700');
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Peso del título';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Normal (400) ';
+ $html .= ' Medium (500) ';
+ $html .= ' Semibold (600) ';
+ $html .= ' Bold (700) ';
+ $html .= ' ';
+ $html .= '
';
+
+ $titleLineHeight = $this->renderer->getFieldValue($componentId, 'typography', 'title_line_height', '1.4');
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Altura de línea';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+
+ $badgeFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'badge_font_size', '0.813rem');
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Tamaño fuente badges';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildSpacingGroup(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Espaciado';
+ $html .= ' ';
+
+ $html .= '
';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildEffectsGroup(string $componentId): string
+ {
+ $html = '
';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Efectos';
+ $html .= ' ';
+
+ $boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', '0 4px 16px rgba(30, 58, 95, 0.25)');
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Sombra del hero';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= '
';
+
+ $titleTextShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'title_text_shadow', '1px 1px 2px rgba(0, 0, 0, 0.2)');
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Sombra del título';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= '
';
+
+ $badgeBackdropBlur = $this->renderer->getFieldValue($componentId, 'visual_effects', 'badge_backdrop_blur', '10px');
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Blur de fondo badges';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildColorPicker(string $id, string $label, string $icon, string $value): string
+ {
+ $html = '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' ' . $label;
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' ' . esc_html(strtoupper($value)) . ' ';
+ $html .= '
';
+
+ return $html;
+ }
+}
diff --git a/Admin/Infrastructure/Api/Wordpress/AdminAjaxHandler.php b/Admin/Infrastructure/Api/Wordpress/AdminAjaxHandler.php
new file mode 100644
index 00000000..96003ccf
--- /dev/null
+++ b/Admin/Infrastructure/Api/Wordpress/AdminAjaxHandler.php
@@ -0,0 +1,596 @@
+ '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
+ */
+ 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'],
+ ];
+ }
+}
diff --git a/Admin/Infrastructure/Api/Wordpress/AdminMenuRegistrar.php b/Admin/Infrastructure/Api/Wordpress/AdminMenuRegistrar.php
new file mode 100644
index 00000000..50ba6cb9
--- /dev/null
+++ b/Admin/Infrastructure/Api/Wordpress/AdminMenuRegistrar.php
@@ -0,0 +1,76 @@
+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 'Error rendering dashboard: ' . esc_html($e->getMessage()) . '
';
+ }
+ }
+
+ public function getCapability(): string
+ {
+ return $this->menuItem->getCapability();
+ }
+
+ public function getSlug(): string
+ {
+ return $this->menuItem->getMenuSlug();
+ }
+}
diff --git a/Admin/Infrastructure/Services/AdminAssetEnqueuer.php b/Admin/Infrastructure/Services/AdminAssetEnqueuer.php
new file mode 100644
index 00000000..936c1aad
--- /dev/null
+++ b/Admin/Infrastructure/Services/AdminAssetEnqueuer.php
@@ -0,0 +1,120 @@
+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')
+ ]
+ );
+ }
+}
diff --git a/Admin/Infrastructure/Ui/AdminDashboardRenderer.php b/Admin/Infrastructure/Ui/AdminDashboardRenderer.php
new file mode 100644
index 00000000..600b9a32
--- /dev/null
+++ b/Admin/Infrastructure/Ui/AdminDashboardRenderer.php
@@ -0,0 +1,160 @@
+ $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>
+ */
+ 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> 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";
+ }
+
+}
diff --git a/Admin/Infrastructure/Ui/Assets/Css/admin-dashboard.css b/Admin/Infrastructure/Ui/Assets/Css/admin-dashboard.css
new file mode 100644
index 00000000..22a3d474
--- /dev/null
+++ b/Admin/Infrastructure/Ui/Assets/Css/admin-dashboard.css
@@ -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;
+ }
+}
diff --git a/Admin/Infrastructure/Ui/Assets/Js/admin-dashboard.js b/Admin/Infrastructure/Ui/Assets/Js/admin-dashboard.js
new file mode 100644
index 00000000..425167ae
--- /dev/null
+++ b/Admin/Infrastructure/Ui/Assets/Js/admin-dashboard.js
@@ -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 = '' + escapeHtml(message) + '
';
+
+ 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 = ' 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 = ' 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 = `
+
+
+
+
+ ${escapeHtml(message)}
+
+
+
+
+ `;
+
+ 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 = `
+
+
+
+
+
+ Mensaje de confirmación
+
+
+
+
+
+ `;
+ 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();
+ });
+ }
+ });
+ }
+
+})();
diff --git a/Admin/Infrastructure/Ui/Views/dashboard.php b/Admin/Infrastructure/Ui/Views/dashboard.php
new file mode 100644
index 00000000..e8d8f08d
--- /dev/null
+++ b/Admin/Infrastructure/Ui/Views/dashboard.php
@@ -0,0 +1,76 @@
+getComponents();
+$firstComponentId = array_key_first($components);
+?>
+
+
+
+
+ $component): ?>
+
+
+
+
+
+
+
+
+
+
+
+ $component):
+ $isFirst = ($componentId === $firstComponentId);
+ $componentSettings = $this->getComponentSettings($componentId);
+ ?>
+
+
+
+ getFormBuilderClass($componentId);
+ if (class_exists($formBuilderClass)) {
+ $formBuilder = new $formBuilderClass($this);
+ echo $formBuilder->buildForm($componentId);
+ } else {
+ echo '
FormBuilder no encontrado: ' . esc_html($formBuilderClass) . '
';
+ }
+ ?>
+
+
+
+
+
+
+
+
+
+ Cancelar
+
+
+
+ Guardar Cambios
+
+
+
+
diff --git a/Admin/Navbar/Infrastructure/Ui/NavbarFormBuilder.php b/Admin/Navbar/Infrastructure/Ui/NavbarFormBuilder.php
new file mode 100644
index 00000000..1a568dc0
--- /dev/null
+++ b/Admin/Navbar/Infrastructure/Ui/NavbarFormBuilder.php
@@ -0,0 +1,517 @@
+buildHeader($componentId);
+
+ // Layout 2 columnas
+ $html .= '';
+ $html .= '
';
+ $html .= $this->buildVisibilityGroup($componentId);
+ $html .= $this->buildLayoutGroup($componentId);
+ $html .= $this->buildBehaviorGroup($componentId);
+ $html .= $this->buildMediaGroup($componentId);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildLinksGroup($componentId);
+ $html .= $this->buildVisualEffectsGroup($componentId);
+ $html .= $this->buildColorsGroup($componentId);
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildHeader(string $componentId): string
+ {
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Activación y Visibilidad';
+ $html .= ' ';
+
+ // Switch: Enabled
+ $enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Activar Navbar ';
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
';
+
+ // Switch: Show on Mobile
+ $showMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Mostrar en Mobile ';
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
';
+
+ // Switch: Show on Desktop
+ $showDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Mostrar en Desktop ';
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
';
+
+ // Select: Show on Pages
+ $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all');
+ $html .= '
';
+ $html .= ' Mostrar en ';
+ $html .= ' ';
+ $html .= ' Todas las páginas ';
+ $html .= ' Solo página de inicio ';
+ $html .= ' Solo posts individuales ';
+ $html .= ' Solo páginas ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Switch: Sticky
+ $sticky = $this->renderer->getFieldValue($componentId, 'visibility', 'sticky_enabled', true);
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Navbar fijo (sticky) ';
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildLayoutGroup(string $componentId): string
+ {
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Layout y Estructura';
+ $html .= ' ';
+
+ // Container Type
+ $containerType = $this->renderer->getFieldValue($componentId, 'layout', 'container_type', 'container');
+ $html .= '
';
+ $html .= ' Tipo de contenedor ';
+ $html .= ' ';
+ $html .= ' Container (ancho fijo) ';
+ $html .= ' Container Fluid (ancho completo) ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Padding Vertical
+ $paddingVertical = $this->renderer->getFieldValue($componentId, 'layout', 'padding_vertical', '0.75rem 0');
+ $html .= '
';
+ $html .= ' Padding vertical ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Z-index
+ $zIndex = $this->renderer->getFieldValue($componentId, 'layout', 'z_index', '1030');
+ $html .= '
';
+ $html .= ' Z-index ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildBehaviorGroup(string $componentId): string
+ {
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Configuración del Menú';
+ $html .= ' ';
+
+ // Menu Location
+ $menuLocation = $this->renderer->getFieldValue($componentId, 'behavior', 'menu_location', 'primary');
+ $html .= '
';
+ $html .= ' Ubicación del menú ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Custom Menu ID
+ $customMenuId = $this->renderer->getFieldValue($componentId, 'behavior', 'custom_menu_id', '0');
+ $html .= '
';
+ $html .= ' ID del menú personalizado ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Enable Dropdowns
+ $enableDropdowns = $this->renderer->getFieldValue($componentId, 'behavior', 'enable_dropdowns', true);
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Habilitar submenús desplegables ';
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
';
+
+ // Mobile Breakpoint
+ $mobileBreakpoint = $this->renderer->getFieldValue($componentId, 'behavior', 'mobile_breakpoint', 'lg');
+ $html .= '
';
+ $html .= ' Breakpoint para menú móvil ';
+ $html .= ' ';
+ $html .= ' Small (576px) ';
+ $html .= ' Medium (768px) ';
+ $html .= ' Large (992px) ';
+ $html .= ' Extra Large (1200px) ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildMediaGroup(string $componentId): string
+ {
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Logo/Marca';
+ $html .= ' ';
+
+ // Show Brand
+ $showBrand = $this->renderer->getFieldValue($componentId, 'media', 'show_brand', false);
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Mostrar logo/marca ';
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
';
+
+ // Use Logo
+ $useLogo = $this->renderer->getFieldValue($componentId, 'media', 'use_logo', false);
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Usar logo (imagen) ';
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
';
+
+ // Logo URL
+ $logoUrl = $this->renderer->getFieldValue($componentId, 'media', 'logo_url', '');
+ $html .= '
';
+ $html .= ' URL del logo ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Logo Height
+ $logoHeight = $this->renderer->getFieldValue($componentId, 'media', 'logo_height', '40px');
+ $html .= '
';
+ $html .= ' Altura del logo ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Brand Text
+ $brandText = $this->renderer->getFieldValue($componentId, 'media', 'brand_text', 'Mi Sitio');
+ $html .= '
';
+ $html .= ' Texto de la marca ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Brand Font Size
+ $brandFontSize = $this->renderer->getFieldValue($componentId, 'media', 'brand_font_size', '1.5rem');
+ $html .= '
';
+ $html .= ' Tamaño de fuente ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Brand Color
+ $brandColor = $this->renderer->getFieldValue($componentId, 'media', 'brand_color', '#FFFFFF');
+ $html .= '
';
+ $html .= ' Color de la marca ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Brand Hover Color
+ $brandHoverColor = $this->renderer->getFieldValue($componentId, 'media', 'brand_hover_color', '#FF8600');
+ $html .= '
';
+ $html .= ' Color hover de la marca ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildLinksGroup(string $componentId): string
+ {
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Estilos de Enlaces';
+ $html .= ' ';
+
+ // Text Color
+ $textColor = $this->renderer->getFieldValue($componentId, 'links', 'text_color', '#FFFFFF');
+ $html .= '
';
+ $html .= ' Color del texto ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Hover Color
+ $hoverColor = $this->renderer->getFieldValue($componentId, 'links', 'hover_color', '#FF8600');
+ $html .= '
';
+ $html .= ' Color hover ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Active Color
+ $activeColor = $this->renderer->getFieldValue($componentId, 'links', 'active_color', '#FF8600');
+ $html .= '
';
+ $html .= ' Color del item activo ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Font Size
+ $fontSize = $this->renderer->getFieldValue($componentId, 'links', 'font_size', '0.9rem');
+ $html .= '
';
+ $html .= ' Tamaño de fuente ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Font Weight
+ $fontWeight = $this->renderer->getFieldValue($componentId, 'links', 'font_weight', '500');
+ $html .= '
';
+ $html .= ' Grosor de fuente ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Padding
+ $padding = $this->renderer->getFieldValue($componentId, 'links', 'padding', '0.5rem 0.65rem');
+ $html .= '
';
+ $html .= ' Padding de enlaces ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Border Radius
+ $borderRadius = $this->renderer->getFieldValue($componentId, 'links', 'border_radius', '4px');
+ $html .= '
';
+ $html .= ' Border radius hover ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Show Underline Effect
+ $showUnderline = $this->renderer->getFieldValue($componentId, 'links', 'show_underline_effect', true);
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Mostrar efecto de subrayado ';
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
';
+
+ // Underline Color
+ $underlineColor = $this->renderer->getFieldValue($componentId, 'links', 'underline_color', '#FF8600');
+ $html .= '
';
+ $html .= ' Color del subrayado ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildVisualEffectsGroup(string $componentId): string
+ {
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Estilos de Dropdown';
+ $html .= ' ';
+
+ // Background Color
+ $bgColor = $this->renderer->getFieldValue($componentId, 'visual_effects', 'background_color', '#FFFFFF');
+ $html .= '
';
+ $html .= ' Fondo de dropdown ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Border Radius
+ $borderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_radius', '8px');
+ $html .= '
';
+ $html .= ' Border radius ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Shadow
+ $shadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'shadow', '0 8px 24px rgba(0, 0, 0, 0.12)');
+ $html .= '
';
+ $html .= ' Sombra del dropdown ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Item Color
+ $itemColor = $this->renderer->getFieldValue($componentId, 'visual_effects', 'item_color', '#495057');
+ $html .= '
';
+ $html .= ' Color de items ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Item Hover Background
+ $itemHoverBg = $this->renderer->getFieldValue($componentId, 'visual_effects', 'item_hover_background', 'rgba(255, 133, 0, 0.1)');
+ $html .= '
';
+ $html .= ' Fondo hover de items ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Item Padding
+ $itemPadding = $this->renderer->getFieldValue($componentId, 'visual_effects', 'item_padding', '0.625rem 1.25rem');
+ $html .= '
';
+ $html .= ' Padding de items ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Dropdown Max Height
+ $dropdownMaxHeight = $this->renderer->getFieldValue($componentId, 'visual_effects', 'dropdown_max_height', '300px');
+ $html .= '
';
+ $html .= ' Altura máxima del dropdown ';
+ $html .= ' ';
+ $html .= ' Si se excede, aparece scroll vertical ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildColorsGroup(string $componentId): string
+ {
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Estilos del Navbar';
+ $html .= ' ';
+
+ // Background Color
+ $navbarBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'background_color', '#1e3a5f');
+ $html .= '
';
+ $html .= ' Color de fondo ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Box Shadow
+ $boxShadow = $this->renderer->getFieldValue($componentId, 'colors', 'box_shadow', '0 4px 12px rgba(30, 58, 95, 0.15)');
+ $html .= '
';
+ $html .= ' Sombra del navbar ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+}
diff --git a/Admin/Navbar/Infrastructure/Ui/navbar-design-preview.html b/Admin/Navbar/Infrastructure/Ui/navbar-design-preview.html
new file mode 100644
index 00000000..403af7f5
--- /dev/null
+++ b/Admin/Navbar/Infrastructure/Ui/navbar-design-preview.html
@@ -0,0 +1,544 @@
+
+
+
+
+
+ Navbar - Preview de Diseño
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Configuración de Navbar
+
+
+ Personaliza el menú de navegación principal del sitio
+
+
+
+
+ Restaurar valores por defecto
+
+
+
+
+
+
+
+
+
+
+
+
+ Activación y Visibilidad
+
+
+
+
+
+
+
+
+
+
+ Activar Navbar
+
+
+
+
+
+
+
+
+
+
+ Mostrar en Mobile (<768px)
+
+
+
+
+
+
+
+
+
+
+ Mostrar en Desktop (≥768px)
+
+
+
+
+
+
+
+
+ Mostrar en
+
+
+ Todas las páginas
+ Solo página de inicio
+ Solo posts individuales
+ Solo páginas
+
+
+
+
+
+
+
+
+
+ Navbar fijo (sticky)
+
+
+
+
+
+
+
+
+
+
+
+ Layout y Estructura
+
+
+
+
+
+
+ Tipo de contenedor
+
+
+ Container (ancho fijo)
+ Container Fluid (ancho completo)
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Configuración del Menú
+
+
+
+
+
+
+
+ Ubicación del menú
+
+
+
+
+
+
+ ID del menú
+
+
+
+
+
+
+
+
+
+
+
+ Habilitar submenús desplegables
+
+
+
+
+
+
+
+
+ Breakpoint para menú móvil
+
+
+ Small (576px)
+ Medium (768px)
+ Large (992px)
+ Extra Large (1200px)
+
+
+
+
+
+
+
+
+
+
+ Logo/Marca
+
+
+
+
+
+
+
+
+ Mostrar logo/marca
+
+
+
+
+
+
+
+
+
+
+ Usar logo (imagen)
+
+
+
Usa una imagen en lugar de texto
+
+
+
+
+
+
+
+
+
+ Texto de la marca
+
+
+ Se muestra si no hay logo
+
+
+
+
+
+
+
+
+
+ Color hover
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Estilos del Navbar
+
+
+
+
+
+
+ Color de fondo
+
+
+ #1E3A5F
+
+
+
+
+
+
+ Sombra del navbar
+
+
+ Sombra CSS (ej: 0 4px 12px rgba(0,0,0,0.15))
+
+
+
+
+
+
+
+
+
+ Estilos de Enlaces
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Mostrar efecto de subrayado
+
+
+
+
+
+
+
+
+ Color del subrayado
+
+
+ #FF8600
+
+
+
+
+
+
+
+
+
+ Estilos de Dropdown
+
+
+
+
+
+
+ Fondo dropdown
+
+
+ #FFFFFF
+
+
+
+
+
+
+
+
+
+
+
+
+ Padding de items
+
+
+ Espaciado interno de items (ej: 0.625rem 1.25rem)
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Admin/RelatedPost/Infrastructure/Ui/RelatedPostFormBuilder.php b/Admin/RelatedPost/Infrastructure/Ui/RelatedPostFormBuilder.php
new file mode 100644
index 00000000..98024fc5
--- /dev/null
+++ b/Admin/RelatedPost/Infrastructure/Ui/RelatedPostFormBuilder.php
@@ -0,0 +1,501 @@
+buildHeader($componentId);
+
+ $html .= '';
+
+ // Columna izquierda
+ $html .= '
';
+ $html .= $this->buildVisibilityGroup($componentId);
+ $html .= $this->buildContentGroup($componentId);
+ $html .= $this->buildLayoutGroup($componentId);
+ $html .= '
';
+
+ // Columna derecha
+ $html .= '
';
+ $html .= $this->buildTypographyGroup($componentId);
+ $html .= $this->buildColorsGroup($componentId);
+ $html .= $this->buildSpacingGroup($componentId);
+ $html .= $this->buildEffectsGroup($componentId);
+ $html .= '
';
+
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildHeader(string $componentId): string
+ {
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Visibilidad';
+ $html .= ' ';
+
+ $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 .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Mostrar en';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Todos ';
+ $html .= ' Solo posts ';
+ $html .= ' Solo paginas ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildContentGroup(string $componentId): string
+ {
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Contenido';
+ $html .= ' ';
+
+ // Section Title
+ $sectionTitle = $this->renderer->getFieldValue($componentId, 'content', 'section_title', 'Descubre Mas Contenido');
+ $html .= '
';
+ $html .= ' Titulo de seccion ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Posts per page
+ $postsPerPage = $this->renderer->getFieldValue($componentId, 'content', 'posts_per_page', '12');
+ $html .= '
';
+ $html .= ' Posts por pagina ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Order by
+ $orderby = $this->renderer->getFieldValue($componentId, 'content', 'orderby', 'rand');
+ $html .= '
';
+ $html .= ' Ordenar por ';
+ $html .= ' ';
+ $html .= ' Aleatorio ';
+ $html .= ' Fecha ';
+ $html .= ' Titulo ';
+ $html .= ' Comentarios ';
+ $html .= ' Orden de menu ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Order direction
+ $order = $this->renderer->getFieldValue($componentId, 'content', 'order', 'DESC');
+ $html .= '
';
+ $html .= ' Direccion ';
+ $html .= ' ';
+ $html .= ' Descendente ';
+ $html .= ' Ascendente ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Show pagination
+ $showPagination = $this->renderer->getFieldValue($componentId, 'content', 'show_pagination', true);
+ $html .= $this->buildSwitch('relatedPostShowPagination', 'Mostrar paginacion', 'bi-three-dots', $showPagination);
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildLayoutGroup(string $componentId): string
+ {
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Disposicion';
+ $html .= ' ';
+
+ // Columns desktop
+ $colsDesktop = $this->renderer->getFieldValue($componentId, 'layout', 'columns_desktop', '3');
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Columnas escritorio';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' 2 columnas ';
+ $html .= ' 3 columnas ';
+ $html .= ' 4 columnas ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Columns tablet
+ $colsTablet = $this->renderer->getFieldValue($componentId, 'layout', 'columns_tablet', '2');
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Columnas tablet';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' 1 columna ';
+ $html .= ' 2 columnas ';
+ $html .= ' 3 columnas ';
+ $html .= ' ';
+ $html .= '
';
+
+ // Columns mobile
+ $colsMobile = $this->renderer->getFieldValue($componentId, 'layout', 'columns_mobile', '1');
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Columnas movil';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' 1 columna ';
+ $html .= ' 2 columnas ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildTypographyGroup(string $componentId): string
+ {
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Tipografia';
+ $html .= ' ';
+
+ $html .= '
';
+
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildColorsGroup(string $componentId): string
+ {
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Colores';
+ $html .= ' ';
+
+ // Seccion
+ $html .= '
Seccion
';
+ $html .= '
';
+
+ $sectionTitleColor = $this->renderer->getFieldValue($componentId, 'colors', 'section_title_color', '#212529');
+ $html .= $this->buildColorPicker('relatedPostSectionTitleColor', 'Titulo seccion', $sectionTitleColor);
+
+ $html .= '
';
+
+ // Cards
+ $html .= '
Cards
';
+ $html .= '
';
+
+ $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 .= '
';
+
+ $html .= '
';
+
+ $cardHoverBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'card_hover_bg_color', '#f8f9fa');
+ $html .= $this->buildColorPicker('relatedPostCardHoverBgColor', 'Fondo hover', $cardHoverBgColor);
+
+ $html .= '
';
+
+ // Paginacion
+ $html .= '
Paginacion
';
+ $html .= '
';
+
+ $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 .= '
';
+
+ $html .= '
';
+
+ $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 .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildSpacingGroup(string $componentId): string
+ {
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Espaciado';
+ $html .= ' ';
+
+ $html .= '
';
+
+ $html .= '
';
+
+ $html .= '
';
+
+ $cardPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'card_padding', '1.5rem');
+ $html .= '
';
+ $html .= ' Padding card ';
+ $html .= ' ';
+ $html .= '
';
+
+ $paginationMarginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'pagination_margin_top', '1rem');
+ $html .= '
';
+ $html .= ' Margen paginacion ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildEffectsGroup(string $componentId): string
+ {
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Efectos Visuales';
+ $html .= ' ';
+
+ $html .= '
';
+
+ $html .= '
';
+ $cardShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_shadow', '0 .125rem .25rem rgba(0,0,0,.075)');
+ $html .= ' Sombra card ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+ $cardHoverShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_hover_shadow', '0 .5rem 1rem rgba(0,0,0,.15)');
+ $html .= ' Sombra hover ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildSwitch(string $id, string $label, string $icon, mixed $checked): string
+ {
+ $checked = $checked === true || $checked === '1' || $checked === 1;
+
+ $html = ' ';
+ $html .= '
';
+ $html .= sprintf(
+ ' ',
+ esc_attr($id),
+ $checked ? 'checked' : ''
+ );
+ $html .= sprintf(
+ ' ',
+ esc_attr($id)
+ );
+ $html .= sprintf(' ', esc_attr($icon));
+ $html .= sprintf(' %s ', esc_html($label));
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildColorPicker(string $id, string $label, string $value): string
+ {
+ $html = ' ';
+ $html .= sprintf(
+ '
%s ',
+ esc_html($label)
+ );
+ $html .= '
';
+ $html .= sprintf(
+ ' ',
+ esc_attr($id),
+ esc_attr($value)
+ );
+ $html .= sprintf(
+ ' %s ',
+ esc_attr($id),
+ esc_html(strtoupper($value))
+ );
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+}
diff --git a/Admin/SocialShare/Infrastructure/Ui/SocialShareFormBuilder.php b/Admin/SocialShare/Infrastructure/Ui/SocialShareFormBuilder.php
new file mode 100644
index 00000000..38bd27b5
--- /dev/null
+++ b/Admin/SocialShare/Infrastructure/Ui/SocialShareFormBuilder.php
@@ -0,0 +1,529 @@
+buildHeader($componentId);
+
+ $html .= '';
+
+ // Columna izquierda
+ $html .= '
';
+ $html .= $this->buildVisibilityGroup($componentId);
+ $html .= $this->buildContentGroup($componentId);
+ $html .= $this->buildEffectsGroup($componentId);
+ $html .= $this->buildTypographyGroup($componentId);
+ $html .= $this->buildSpacingGroup($componentId);
+ $html .= '
';
+
+ // Columna derecha
+ $html .= '
';
+ $html .= $this->buildNetworksGroup($componentId);
+ $html .= $this->buildColorsGroup($componentId);
+ $html .= '
';
+
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildHeader(string $componentId): string
+ {
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Visibilidad';
+ $html .= ' ';
+
+ // 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 .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Mostrar en';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Todos ';
+ $html .= ' Solo posts ';
+ $html .= ' Solo paginas ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildContentGroup(string $componentId): string
+ {
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Contenido';
+ $html .= ' ';
+
+ // 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 .= '
';
+ $html .= ' Texto etiqueta ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildNetworksGroup(string $componentId): string
+ {
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Redes Sociales';
+ $html .= ' ';
+ $html .= '
Configura las redes sociales y sus URLs
';
+
+ // 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 .= '
';
+ $html .= '
';
+
+ 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 = ' ';
+
+ return $html;
+ }
+
+ private function buildColorsGroup(string $componentId): string
+ {
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Colores';
+ $html .= ' ';
+
+ // Colores generales
+ $html .= '
General
';
+ $html .= '
';
+
+ $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 .= '
';
+
+ $html .= '
';
+
+ $buttonBackground = $this->renderer->getFieldValue($componentId, 'colors', 'button_background', '#ffffff');
+ $html .= $this->buildColorPicker('socialShareButtonBg', 'Fondo botones', $buttonBackground);
+
+ $html .= '
';
+
+ // Colores por red social
+ $html .= '
Redes Sociales
';
+ $html .= '
';
+
+ $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 .= '
';
+
+ $html .= '
';
+
+ $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 .= '
';
+
+ $html .= '
';
+
+ $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 .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildTypographyGroup(string $componentId): string
+ {
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Tipografia';
+ $html .= ' ';
+
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildSpacingGroup(string $componentId): string
+ {
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Espaciado';
+ $html .= ' ';
+
+ $html .= '
';
+
+ $html .= '
';
+
+ $html .= '
';
+
+ $html .= '
';
+
+ // button_padding
+ $buttonPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'button_padding', '0.25rem 0.5rem');
+ $html .= '
';
+ $html .= ' Padding botones ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildEffectsGroup(string $componentId): string
+ {
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Efectos Visuales';
+ $html .= ' ';
+
+ $html .= '
';
+
+ $html .= '
';
+
+ $html .= '
';
+
+ // hover_box_shadow
+ $hoverBoxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'hover_box_shadow', '0 4px 12px rgba(0, 0, 0, 0.15)');
+ $html .= '
';
+ $html .= ' Sombra hover ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ 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 = ' ';
+ $html .= '
';
+ $html .= sprintf(
+ ' ',
+ esc_attr($id),
+ $checked ? 'checked' : ''
+ );
+ $html .= sprintf(
+ ' ',
+ esc_attr($id)
+ );
+ $html .= sprintf(' ', esc_attr($icon));
+ $html .= sprintf(' %s ', esc_html($label));
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildColorPicker(string $id, string $label, string $value): string
+ {
+ $html = ' ';
+ $html .= sprintf(
+ '
%s ',
+ esc_html($label)
+ );
+ $html .= '
';
+ $html .= sprintf(
+ ' ',
+ esc_attr($id),
+ esc_attr($value)
+ );
+ $html .= sprintf(
+ ' %s ',
+ esc_attr($id),
+ esc_html(strtoupper($value))
+ );
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+}
diff --git a/Admin/TableOfContents/Infrastructure/Ui/TableOfContentsFormBuilder.php b/Admin/TableOfContents/Infrastructure/Ui/TableOfContentsFormBuilder.php
new file mode 100644
index 00000000..cb4238ce
--- /dev/null
+++ b/Admin/TableOfContents/Infrastructure/Ui/TableOfContentsFormBuilder.php
@@ -0,0 +1,588 @@
+buildHeader($componentId);
+
+ $html .= '';
+
+ // Columna izquierda
+ $html .= '
';
+ $html .= $this->buildVisibilityGroup($componentId);
+ $html .= $this->buildContentGroup($componentId);
+ $html .= $this->buildBehaviorGroup($componentId);
+ $html .= $this->buildEffectsGroup($componentId);
+ $html .= '
';
+
+ // Columna derecha
+ $html .= '
';
+ $html .= $this->buildTypographyGroup($componentId);
+ $html .= $this->buildColorsGroup($componentId);
+ $html .= $this->buildSpacingGroup($componentId);
+ $html .= '
';
+
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildHeader(string $componentId): string
+ {
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Visibilidad';
+ $html .= ' ';
+
+ // 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 .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Mostrar en';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Todas las paginas ';
+ $html .= ' Solo posts ';
+ $html .= ' Solo paginas ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildContentGroup(string $componentId): string
+ {
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Contenido';
+ $html .= ' ';
+
+ // title
+ $title = $this->renderer->getFieldValue($componentId, 'content', 'title', 'Tabla de Contenido');
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Titulo';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= '
';
+
+ // auto_generate
+ $autoGenerate = $this->renderer->getFieldValue($componentId, 'content', 'auto_generate', true);
+ $html .= $this->buildSwitch('tocAutoGenerate', 'Generar automaticamente', 'bi-magic', $autoGenerate);
+ $html .= '
Genera TOC desde los encabezados del contenido ';
+
+ // heading_levels
+ $headingLevels = $this->renderer->getFieldValue($componentId, 'content', 'heading_levels', 'h2,h3');
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Niveles de encabezados';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Separados por coma: h2,h3,h4 ';
+ $html .= '
';
+
+ // smooth_scroll
+ $smoothScroll = $this->renderer->getFieldValue($componentId, 'content', 'smooth_scroll', true);
+ $html .= $this->buildSwitch('tocSmoothScroll', 'Scroll suave', 'bi-arrow-down-circle', $smoothScroll);
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildBehaviorGroup(string $componentId): string
+ {
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Comportamiento';
+ $html .= ' ';
+
+ // 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 .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Offset de scroll (px)';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= '
';
+
+ // max_height
+ $maxHeight = $this->renderer->getFieldValue($componentId, 'behavior', 'max_height', 'calc(100vh - 71px - 10px - 250px - 15px - 15px)');
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Altura maxima';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildTypographyGroup(string $componentId): string
+ {
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Tipografia';
+ $html .= ' ';
+
+ $html .= '
';
+
+ $html .= '
';
+
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildColorsGroup(string $componentId): string
+ {
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Colores';
+ $html .= ' ';
+
+ // Colores principales
+ $html .= '
Contenedor
';
+ $html .= '
';
+
+ $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 .= '
';
+
+ // Colores del titulo
+ $html .= '
Titulo
';
+ $html .= '
';
+
+ $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 .= '
';
+
+ // Colores de enlaces
+ $html .= '
Enlaces
';
+ $html .= '
';
+
+ $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 .= '
';
+
+ $html .= '
';
+
+ $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 .= '
';
+
+ // Colores de activo
+ $html .= '
Estado Activo
';
+ $html .= '
';
+
+ $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 .= '
';
+
+ // Colores de scrollbar
+ $html .= '
Scrollbar
';
+ $html .= '
';
+
+ $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 .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildSpacingGroup(string $componentId): string
+ {
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Espaciado';
+ $html .= ' ';
+
+ $html .= '
';
+
+ $html .= '
';
+
+ $html .= '
';
+
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildEffectsGroup(string $componentId): string
+ {
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Efectos Visuales';
+ $html .= ' ';
+
+ $html .= '
';
+
+ // box_shadow
+ $boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', '0 2px 8px rgba(0, 0, 0, 0.08)');
+ $html .= '
';
+ $html .= ' Sombra ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildSwitch(string $id, string $label, string $icon, bool $checked): string
+ {
+ $html = ' ';
+ $html .= '
';
+ $html .= sprintf(
+ ' ',
+ esc_attr($id),
+ $checked ? 'checked' : ''
+ );
+ $html .= sprintf(
+ ' ',
+ esc_attr($id)
+ );
+ $html .= sprintf(' ', esc_attr($icon));
+ $html .= sprintf(' %s ', esc_html($label));
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildColorPicker(string $id, string $label, string $value): string
+ {
+ $html = ' ';
+ $html .= sprintf(
+ '
%s ',
+ esc_html($label)
+ );
+ $html .= '
';
+ $html .= sprintf(
+ ' ',
+ esc_attr($id),
+ esc_attr($value)
+ );
+ $html .= sprintf(
+ ' %s ',
+ esc_attr($id),
+ esc_html(strtoupper($value))
+ );
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+}
diff --git a/Admin/TopNotificationBar/Infrastructure/Ui/TopNotificationBarFormBuilder.php b/Admin/TopNotificationBar/Infrastructure/Ui/TopNotificationBarFormBuilder.php
new file mode 100644
index 00000000..b5087dc6
--- /dev/null
+++ b/Admin/TopNotificationBar/Infrastructure/Ui/TopNotificationBarFormBuilder.php
@@ -0,0 +1,308 @@
+buildHeader($componentId);
+
+ // Layout 2 columnas
+ $html .= '';
+ $html .= '
';
+ $html .= $this->buildVisibilityGroup($componentId);
+ $html .= $this->buildContentGroup($componentId);
+ $html .= '
';
+ $html .= '
';
+ $html .= $this->buildColorsGroup($componentId);
+ $html .= $this->buildTypographyAndSpacingGroup($componentId);
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildHeader(string $componentId): string
+ {
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Activación y Visibilidad';
+ $html .= ' ';
+
+ // Switch: Enabled
+ $enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Activar TopBar ';
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
';
+
+ // Switch: Show on Mobile
+ $showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Mostrar en Mobile (<768px) ';
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
';
+
+ // Switch: Show on Desktop
+ $showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Mostrar en Desktop (≥768px) ';
+ $html .= ' ';
+ $html .= '
';
+ $html .= '
';
+
+ // Select: Show on Pages
+ $showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all');
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Mostrar en';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Todas las páginas ';
+ $html .= ' Solo página de inicio ';
+ $html .= ' Solo posts individuales ';
+ $html .= ' Solo páginas ';
+ $html .= ' ';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildContentGroup(string $componentId): string
+ {
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Contenido';
+ $html .= ' ';
+
+ // icon_class + label_text (row)
+ $html .= '
';
+
+ // 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 .= '
';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Mensaje';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' Máximo 200 caracteres ';
+ $html .= '
';
+
+ // link_text + link_url (row)
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildColorsGroup(string $componentId): string
+ {
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Colores';
+ $html .= ' ';
+
+ // Grid 2x3 de color pickers
+ $html .= '
';
+
+ // 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 .= '
';
+
+ // Row 2 de color pickers
+ $html .= '
';
+
+ // 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 .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildTypographyAndSpacingGroup(string $componentId): string
+ {
+ $html = '';
+ $html .= '
';
+ $html .= '
';
+ $html .= ' ';
+ $html .= ' Tipografía y Espaciado';
+ $html .= ' ';
+
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function buildColorPicker(string $id, string $label, string $icon, string $value): string
+ {
+ $html = ' ';
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' ' . $label;
+ $html .= ' ';
+ $html .= ' ';
+ $html .= ' ' . esc_html(strtoupper($value)) . ' ';
+ $html .= '
';
+
+ return $html;
+ }
+}
diff --git a/Admin/TopNotificationBar/Infrastructure/Ui/top-bar-design-preview.html b/Admin/TopNotificationBar/Infrastructure/Ui/top-bar-design-preview.html
new file mode 100644
index 00000000..3b536771
--- /dev/null
+++ b/Admin/TopNotificationBar/Infrastructure/Ui/top-bar-design-preview.html
@@ -0,0 +1,283 @@
+
+
+
+
+
+ TopBar - Preview de Diseño
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Configuración de TopBar
+
+
+ Personaliza la barra de notificación superior del sitio
+
+
+
+
+ Restaurar valores por defecto
+
+
+
+
+
+
+
+
+
+
+
+
+ Activación y Visibilidad
+
+
+
+
+
+
+
+
+
+
+ Activar TopBar
+
+
+
+
+
+
+
+
+
+
+ Mostrar en Mobile (<768px)
+
+
+
+
+
+
+
+
+
+
+ Mostrar en Desktop (≥768px)
+
+
+
+
+
+
+
+
+ Mostrar en
+
+
+ Todas las páginas
+ Solo página de inicio
+ Solo posts individuales
+ Solo páginas
+
+
+
+
+
+
+
+
+
+
+ Contenido
+
+
+
+
+
+
+
+
+
+ Mensaje
+
+
+ Máximo 200 caracteres
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Estilos - Colores
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Estilos - Tamaños
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Component/Application/UseCases/SyncSchema/.gitkeep b/Public/.gitkeep
similarity index 100%
rename from src/Component/Application/UseCases/SyncSchema/.gitkeep
rename to Public/.gitkeep
diff --git a/Public/ContactForm/Infrastructure/Api/WordPress/ContactFormAjaxHandler.php b/Public/ContactForm/Infrastructure/Api/WordPress/ContactFormAjaxHandler.php
new file mode 100644
index 00000000..23c69ba3
--- /dev/null
+++ b/Public/ContactForm/Infrastructure/Api/WordPress/ContactFormAjaxHandler.php
@@ -0,0 +1,303 @@
+ __('Error de seguridad. Por favor recarga la pagina.', 'roi-theme')
+ ], 403);
+ return;
+ }
+
+ // 2. Rate limiting basico (1 envio por IP cada 30 segundos)
+ if (!$this->checkRateLimit()) {
+ wp_send_json_error([
+ 'message' => __('Por favor espera un momento antes de enviar otro mensaje.', 'roi-theme')
+ ], 429);
+ return;
+ }
+
+ // 3. Sanitizar y validar inputs
+ $formData = $this->sanitizeFormData($_POST);
+ $validation = $this->validateFormData($formData);
+
+ if (!$validation['valid']) {
+ wp_send_json_error([
+ 'message' => $validation['message'],
+ 'errors' => $validation['errors']
+ ], 422);
+ return;
+ }
+
+ // 4. Obtener configuracion del componente (incluye webhook URL)
+ $settings = $this->settingsRepository->getComponentSettings(self::COMPONENT_NAME);
+
+ if (empty($settings)) {
+ wp_send_json_error([
+ 'message' => __('Error de configuracion. Contacta al administrador.', 'roi-theme')
+ ], 500);
+ return;
+ }
+
+ $integration = $settings['integration'] ?? [];
+ $webhookUrl = $integration['webhook_url'] ?? '';
+ $webhookMethod = $integration['webhook_method'] ?? 'POST';
+ $includePageUrl = $this->toBool($integration['include_page_url'] ?? true);
+ $includeTimestamp = $this->toBool($integration['include_timestamp'] ?? true);
+
+ if (empty($webhookUrl)) {
+ // Si no hay webhook configurado, simular exito para UX
+ // pero loguear warning para admin
+ error_log('ROI Theme Contact Form: No webhook URL configured');
+ wp_send_json_success([
+ 'message' => $this->getSuccessMessage($settings)
+ ]);
+ return;
+ }
+
+ // 5. Preparar payload para webhook
+ $payload = $this->preparePayload($formData, $includePageUrl, $includeTimestamp);
+
+ // 6. Enviar a webhook
+ $result = $this->sendToWebhook($webhookUrl, $webhookMethod, $payload);
+
+ if ($result['success']) {
+ wp_send_json_success([
+ 'message' => $this->getSuccessMessage($settings)
+ ]);
+ } else {
+ error_log('ROI Theme Contact Form webhook error: ' . $result['error']);
+ wp_send_json_error([
+ 'message' => $this->getErrorMessage($settings)
+ ], 500);
+ }
+ }
+
+ /**
+ * Sanitizar datos del formulario
+ */
+ private function sanitizeFormData(array $post): array
+ {
+ return [
+ 'fullName' => sanitize_text_field($post['fullName'] ?? ''),
+ 'company' => sanitize_text_field($post['company'] ?? ''),
+ 'whatsapp' => sanitize_text_field($post['whatsapp'] ?? ''),
+ 'email' => sanitize_email($post['email'] ?? ''),
+ 'message' => sanitize_textarea_field($post['message'] ?? ''),
+ ];
+ }
+
+ /**
+ * Validar datos del formulario
+ */
+ private function validateFormData(array $data): array
+ {
+ $errors = [];
+
+ // Nombre requerido
+ if (empty($data['fullName'])) {
+ $errors['fullName'] = __('El nombre es obligatorio', 'roi-theme');
+ }
+
+ // WhatsApp requerido
+ if (empty($data['whatsapp'])) {
+ $errors['whatsapp'] = __('El WhatsApp es obligatorio', 'roi-theme');
+ }
+
+ // Email requerido y valido
+ if (empty($data['email'])) {
+ $errors['email'] = __('El email es obligatorio', 'roi-theme');
+ } elseif (!is_email($data['email'])) {
+ $errors['email'] = __('Por favor ingresa un email valido', 'roi-theme');
+ }
+
+ if (!empty($errors)) {
+ return [
+ 'valid' => false,
+ 'message' => __('Por favor corrige los errores del formulario', 'roi-theme'),
+ 'errors' => $errors
+ ];
+ }
+
+ return ['valid' => true, 'message' => '', 'errors' => []];
+ }
+
+ /**
+ * Preparar payload para webhook
+ */
+ private function preparePayload(array $formData, bool $includePageUrl, bool $includeTimestamp): array
+ {
+ $payload = [
+ 'fullName' => $formData['fullName'],
+ 'company' => $formData['company'],
+ 'whatsapp' => $formData['whatsapp'],
+ 'email' => $formData['email'],
+ 'message' => $formData['message'],
+ ];
+
+ if ($includePageUrl) {
+ $payload['pageUrl'] = sanitize_url($_POST['pageUrl'] ?? '');
+ $payload['pageTitle'] = sanitize_text_field($_POST['pageTitle'] ?? '');
+ }
+
+ if ($includeTimestamp) {
+ $payload['timestamp'] = current_time('c');
+ $payload['timezone'] = wp_timezone_string();
+ }
+
+ // Metadata adicional util para el webhook
+ $payload['source'] = 'contact-form';
+ $payload['siteName'] = get_bloginfo('name');
+ $payload['siteUrl'] = home_url();
+
+ return $payload;
+ }
+
+ /**
+ * Enviar datos al webhook
+ */
+ private function sendToWebhook(string $url, string $method, array $payload): array
+ {
+ $args = [
+ 'method' => strtoupper($method),
+ 'timeout' => 30,
+ 'redirection' => 5,
+ 'httpversion' => '1.1',
+ 'headers' => [
+ 'Content-Type' => 'application/json',
+ 'Accept' => 'application/json',
+ ],
+ ];
+
+ if ($method === 'POST') {
+ $args['body'] = wp_json_encode($payload);
+ } else {
+ $url = add_query_arg($payload, $url);
+ }
+
+ $response = wp_remote_request($url, $args);
+
+ if (is_wp_error($response)) {
+ return [
+ 'success' => false,
+ 'error' => $response->get_error_message()
+ ];
+ }
+
+ $statusCode = wp_remote_retrieve_response_code($response);
+
+ // Considerar 2xx como exito
+ if ($statusCode >= 200 && $statusCode < 300) {
+ return ['success' => true, 'error' => ''];
+ }
+
+ return [
+ 'success' => false,
+ 'error' => sprintf('HTTP %d: %s', $statusCode, wp_remote_retrieve_response_message($response))
+ ];
+ }
+
+ /**
+ * Rate limiting basico por IP
+ */
+ private function checkRateLimit(): bool
+ {
+ $ip = $this->getClientIP();
+ $transientKey = 'roi_contact_form_' . md5($ip);
+ $lastSubmit = get_transient($transientKey);
+
+ if ($lastSubmit !== false) {
+ return false;
+ }
+
+ set_transient($transientKey, time(), 30);
+ return true;
+ }
+
+ /**
+ * Obtener IP del cliente
+ */
+ private function getClientIP(): string
+ {
+ $ip = '';
+
+ if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
+ $ip = sanitize_text_field($_SERVER['HTTP_CLIENT_IP']);
+ } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
+ $ip = sanitize_text_field(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0]);
+ } elseif (!empty($_SERVER['REMOTE_ADDR'])) {
+ $ip = sanitize_text_field($_SERVER['REMOTE_ADDR']);
+ }
+
+ return $ip;
+ }
+
+ /**
+ * Obtener mensaje de exito desde configuracion
+ */
+ private function getSuccessMessage(array $data): string
+ {
+ $messages = $data['messages'] ?? [];
+ return $messages['success_message'] ?? __('¡Gracias por contactarnos! Te responderemos pronto.', 'roi-theme');
+ }
+
+ /**
+ * Obtener mensaje de error desde configuracion
+ */
+ private function getErrorMessage(array $data): string
+ {
+ $messages = $data['messages'] ?? [];
+ return $messages['error_message'] ?? __('Hubo un error al enviar el mensaje. Por favor intenta de nuevo.', 'roi-theme');
+ }
+
+ /**
+ * Convertir valor a boolean
+ */
+ private function toBool($value): bool
+ {
+ return $value === true || $value === '1' || $value === 1;
+ }
+}
diff --git a/Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php b/Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php
new file mode 100644
index 00000000..dae7b4e7
--- /dev/null
+++ b/Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php
@@ -0,0 +1,461 @@
+getData();
+
+ if (!$this->isEnabled($data)) {
+ return '';
+ }
+
+ if (!$this->shouldShowOnCurrentPage($data)) {
+ return '';
+ }
+
+ $visibilityClass = $this->getVisibilityClass($data);
+ if ($visibilityClass === null) {
+ return '';
+ }
+
+ $css = $this->generateCSS($data);
+ $html = $this->buildHTML($data, $visibilityClass);
+ $js = $this->buildJS($data);
+
+ return sprintf("\n%s\n", $css, $html, $js);
+ }
+
+ public function supports(string $componentType): bool
+ {
+ return $componentType === 'contact-form';
+ }
+
+ private function isEnabled(array $data): bool
+ {
+ $value = $data['visibility']['is_enabled'] ?? false;
+ return $value === true || $value === '1' || $value === 1;
+ }
+
+ private function shouldShowOnCurrentPage(array $data): bool
+ {
+ $showOn = $data['visibility']['show_on_pages'] ?? 'all';
+
+ switch ($showOn) {
+ case 'all':
+ return true;
+ case 'posts':
+ return is_single();
+ case 'pages':
+ return is_page();
+ default:
+ return true;
+ }
+ }
+
+ private function getVisibilityClass(array $data): ?string
+ {
+ $showDesktop = $data['visibility']['show_on_desktop'] ?? true;
+ $showDesktop = $showDesktop === true || $showDesktop === '1' || $showDesktop === 1;
+ $showMobile = $data['visibility']['show_on_mobile'] ?? true;
+ $showMobile = $showMobile === true || $showMobile === '1' || $showMobile === 1;
+
+ if (!$showDesktop && !$showMobile) {
+ return null;
+ }
+ if (!$showDesktop && $showMobile) {
+ return 'd-lg-none';
+ }
+ if ($showDesktop && !$showMobile) {
+ return 'd-none d-lg-block';
+ }
+ return '';
+ }
+
+ private function generateCSS(array $data): string
+ {
+ $colors = $data['colors'] ?? [];
+ $spacing = $data['spacing'] ?? [];
+ $effects = $data['visual_effects'] ?? [];
+
+ $cssRules = [];
+
+ // Section background
+ $sectionBgColor = $colors['section_bg_color'] ?? 'rgba(108, 117, 125, 0.25)';
+ $sectionPaddingY = $spacing['section_padding_y'] ?? '3rem';
+ $sectionMarginTop = $spacing['section_margin_top'] ?? '3rem';
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section', [
+ 'background-color' => $sectionBgColor,
+ 'padding-top' => $sectionPaddingY,
+ 'padding-bottom' => $sectionPaddingY,
+ 'margin-top' => $sectionMarginTop,
+ ]);
+
+ // Title
+ $titleColor = $colors['title_color'] ?? '#212529';
+ $titleMarginBottom = $spacing['title_margin_bottom'] ?? '0.75rem';
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .contact-title', [
+ 'color' => $titleColor,
+ 'margin-bottom' => $titleMarginBottom,
+ ]);
+
+ // Description
+ $descColor = $colors['description_color'] ?? '#212529';
+ $descMarginBottom = $spacing['description_margin_bottom'] ?? '1.5rem';
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .contact-description', [
+ 'color' => $descColor,
+ 'margin-bottom' => $descMarginBottom,
+ ]);
+
+ // Icons
+ $iconColor = $colors['icon_color'] ?? '#FF8600';
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .contact-icon', [
+ 'color' => $iconColor,
+ ]);
+
+ // Info labels and values
+ $infoLabelColor = $colors['info_label_color'] ?? '#212529';
+ $infoValueColor = $colors['info_value_color'] ?? '#6c757d';
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .info-label', [
+ 'color' => $infoLabelColor,
+ ]);
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .info-value', [
+ 'color' => $infoValueColor,
+ ]);
+
+ // Form inputs
+ $inputBorderColor = $colors['input_border_color'] ?? '#dee2e6';
+ $inputFocusBorder = $colors['input_focus_border'] ?? '#FF8600';
+ $inputBorderRadius = $effects['input_border_radius'] ?? '6px';
+ $transitionDuration = $effects['transition_duration'] ?? '0.3s';
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .form-control', [
+ 'border-color' => $inputBorderColor,
+ 'border-radius' => $inputBorderRadius,
+ 'transition' => "all {$transitionDuration} ease",
+ ]);
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .form-control:focus', [
+ 'border-color' => $inputFocusBorder,
+ 'box-shadow' => "0 0 0 0.2rem rgba(255, 134, 0, 0.25)",
+ 'outline' => 'none',
+ ]);
+
+ // Submit button
+ $buttonBgColor = $colors['button_bg_color'] ?? '#FF8600';
+ $buttonTextColor = $colors['button_text_color'] ?? '#ffffff';
+ $buttonHoverBg = $colors['button_hover_bg'] ?? '#e67a00';
+ $buttonBorderRadius = $effects['button_border_radius'] ?? '6px';
+ $buttonPadding = $effects['button_padding'] ?? '0.75rem 2rem';
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .btn-contact-submit', [
+ 'background-color' => $buttonBgColor,
+ 'color' => $buttonTextColor,
+ 'font-weight' => '600',
+ 'padding' => $buttonPadding,
+ 'border' => 'none',
+ 'border-radius' => $buttonBorderRadius,
+ 'transition' => "all {$transitionDuration} ease",
+ ]);
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .btn-contact-submit:hover', [
+ 'background-color' => $buttonHoverBg,
+ 'color' => $buttonTextColor,
+ ]);
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .btn-contact-submit:disabled', [
+ 'opacity' => '0.7',
+ 'cursor' => 'not-allowed',
+ ]);
+
+ // Success/Error messages
+ $successBgColor = $colors['success_bg_color'] ?? '#d1e7dd';
+ $successTextColor = $colors['success_text_color'] ?? '#0f5132';
+ $errorBgColor = $colors['error_bg_color'] ?? '#f8d7da';
+ $errorTextColor = $colors['error_text_color'] ?? '#842029';
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .alert-success', [
+ 'background-color' => $successBgColor,
+ 'color' => $successTextColor,
+ 'border-color' => $successBgColor,
+ ]);
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-contact-form-section .alert-danger', [
+ 'background-color' => $errorBgColor,
+ 'color' => $errorTextColor,
+ 'border-color' => $errorBgColor,
+ ]);
+
+ return implode("\n", $cssRules);
+ }
+
+ private function buildHTML(array $data, string $visibilityClass): string
+ {
+ $content = $data['content'] ?? [];
+ $contactInfo = $data['contact_info'] ?? [];
+ $formLabels = $data['form_labels'] ?? [];
+ $effects = $data['visual_effects'] ?? [];
+
+ // Content
+ $sectionTitle = $content['section_title'] ?? '¿Tienes alguna pregunta?';
+ $sectionDesc = $content['section_description'] ?? 'Completa el formulario y nuestro equipo te responderá en menos de 24 horas.';
+ $submitText = $content['submit_button_text'] ?? 'Enviar Mensaje';
+ $submitIcon = $content['submit_button_icon'] ?? 'bi-send-fill';
+
+ // Contact info
+ $showContactInfo = $contactInfo['show_contact_info'] ?? true;
+ $showContactInfo = $showContactInfo === true || $showContactInfo === '1' || $showContactInfo === 1;
+
+ // Form labels/placeholders
+ $fullnamePlaceholder = $formLabels['fullname_placeholder'] ?? 'Nombre completo *';
+ $companyPlaceholder = $formLabels['company_placeholder'] ?? 'Empresa';
+ $whatsappPlaceholder = $formLabels['whatsapp_placeholder'] ?? 'WhatsApp *';
+ $emailPlaceholder = $formLabels['email_placeholder'] ?? 'Correo electrónico *';
+ $messagePlaceholder = $formLabels['message_placeholder'] ?? '¿En qué podemos ayudarte?';
+
+ $textareaRows = $effects['textarea_rows'] ?? '4';
+
+ // Container class
+ $containerClass = 'roi-contact-form-section';
+ if (!empty($visibilityClass)) {
+ $containerClass .= ' ' . $visibilityClass;
+ }
+
+ // Nonce for AJAX security
+ $nonce = wp_create_nonce('roi_contact_form_nonce');
+
+ $html = sprintf('', esc_attr($containerClass));
+ $html .= '';
+ $html .= '
';
+ $html .= '
';
+ $html .= '
';
+
+ // Left column - Contact info
+ $html .= '
';
+ $html .= sprintf('
', esc_html($sectionTitle));
+ $html .= sprintf('
%s
', esc_html($sectionDesc));
+
+ if ($showContactInfo) {
+ $html .= $this->buildContactInfoHTML($contactInfo);
+ }
+
+ $html .= '
';
+
+ // Right column - Form
+ $html .= '
';
+ $html .= sprintf('
';
+ $html .= '
'; // .col-lg-7
+
+ $html .= '
'; // .row
+ $html .= '
'; // .col-lg-10
+ $html .= '
'; // .row justify-content-center
+ $html .= '
'; // .container
+ $html .= ' ';
+
+ return $html;
+ }
+
+ private function buildContactInfoHTML(array $contactInfo): string
+ {
+ $phoneLabel = $contactInfo['phone_label'] ?? 'Teléfono';
+ $phoneValue = $contactInfo['phone_value'] ?? '+52 55 1234 5678';
+ $emailLabel = $contactInfo['email_label'] ?? 'Email';
+ $emailValue = $contactInfo['email_value'] ?? 'contacto@apumexico.com';
+ $locationLabel = $contactInfo['location_label'] ?? 'Ubicación';
+ $locationValue = $contactInfo['location_value'] ?? 'Ciudad de México, México';
+
+ $html = '';
+
+ return $html;
+ }
+
+ private function buildJS(array $data): string
+ {
+ $messages = $data['messages'] ?? [];
+ $content = $data['content'] ?? [];
+
+ $successMessage = $messages['success_message'] ?? '¡Gracias por contactarnos! Te responderemos pronto.';
+ $errorMessage = $messages['error_message'] ?? 'Hubo un error al enviar el mensaje. Por favor intenta de nuevo.';
+ $sendingMessage = $messages['sending_message'] ?? 'Enviando...';
+ $submitText = $content['submit_button_text'] ?? 'Enviar Mensaje';
+ $submitIcon = $content['submit_button_icon'] ?? 'bi-send-fill';
+
+ // AJAX URL for WordPress
+ $ajaxUrl = admin_url('admin-ajax.php');
+
+ $js = <<getData();
+
+ if (!$this->isEnabled($data)) {
+ return '';
+ }
+
+ if (!$this->shouldShowOnCurrentPage($data)) {
+ return '';
+ }
+
+ $css = $this->generateCSS($data);
+ $html = $this->buildHTML($data);
+ $script = $this->buildScript();
+
+ return sprintf("\n%s\n%s", $css, $html, $script);
+ }
+
+ public function supports(string $componentType): bool
+ {
+ return $componentType === 'cta-box-sidebar';
+ }
+
+ private function isEnabled(array $data): bool
+ {
+ return ($data['visibility']['is_enabled'] ?? false) === true;
+ }
+
+ private function shouldShowOnCurrentPage(array $data): bool
+ {
+ $showOn = $data['visibility']['show_on_pages'] ?? 'posts';
+
+ switch ($showOn) {
+ case 'all':
+ return true;
+ case 'posts':
+ return is_single();
+ case 'pages':
+ return is_page();
+ default:
+ return true;
+ }
+ }
+
+ private function generateCSS(array $data): string
+ {
+ $colors = $data['colors'] ?? [];
+ $spacing = $data['spacing'] ?? [];
+ $typography = $data['typography'] ?? [];
+ $effects = $data['visual_effects'] ?? [];
+ $behavior = $data['behavior'] ?? [];
+ $visibility = $data['visibility'] ?? [];
+
+ $cssRules = [];
+ $transitionDuration = $effects['transition_duration'] ?? '0.3s';
+
+ // Container styles - Match template exactly (height: 250px, flexbox centering)
+ $cssRules[] = $this->cssGenerator->generate('.cta-box-sidebar', [
+ 'background' => $colors['background_color'] ?? '#FF8600',
+ 'border-radius' => $effects['border_radius'] ?? '8px',
+ 'padding' => $spacing['container_padding'] ?? '24px',
+ 'text-align' => $behavior['text_align'] ?? 'center',
+ 'box-shadow' => $effects['box_shadow'] ?? '0 4px 12px rgba(255, 133, 0, 0.2)',
+ 'margin-top' => '0',
+ 'margin-bottom' => '15px',
+ 'height' => '250px',
+ 'display' => 'flex',
+ 'flex-direction' => 'column',
+ 'justify-content' => 'center',
+ ]);
+
+ // Title styles
+ $cssRules[] = $this->cssGenerator->generate('.cta-box-sidebar .cta-box-title', [
+ 'color' => $colors['title_color'] ?? '#ffffff',
+ 'font-weight' => $typography['title_font_weight'] ?? '700',
+ 'font-size' => $typography['title_font_size'] ?? '1.25rem',
+ 'margin-bottom' => $spacing['title_margin_bottom'] ?? '1rem',
+ 'margin-top' => '0',
+ ]);
+
+ // Description styles
+ $cssRules[] = $this->cssGenerator->generate('.cta-box-sidebar .cta-box-text', [
+ 'color' => $colors['description_color'] ?? 'rgba(255, 255, 255, 0.95)',
+ 'font-size' => $typography['description_font_size'] ?? '0.9rem',
+ 'margin-bottom' => $spacing['description_margin_bottom'] ?? '1rem',
+ ]);
+
+ // Button styles
+ $cssRules[] = $this->cssGenerator->generate('.cta-box-sidebar .btn-cta-box', [
+ 'background-color' => $colors['button_background_color'] ?? '#ffffff',
+ 'color' => $colors['button_text_color'] ?? '#FF8600',
+ 'font-weight' => $typography['button_font_weight'] ?? '700',
+ 'font-size' => $typography['button_font_size'] ?? '1rem',
+ 'border' => 'none',
+ 'padding' => $spacing['button_padding'] ?? '0.75rem 1.5rem',
+ 'border-radius' => $effects['button_border_radius'] ?? '8px',
+ 'transition' => "all {$transitionDuration} ease",
+ 'cursor' => 'pointer',
+ 'display' => 'inline-flex',
+ 'align-items' => 'center',
+ 'justify-content' => 'center',
+ 'width' => '100%',
+ ]);
+
+ // Button hover styles (template uses --color-navy-primary = #1e3a5f)
+ $cssRules[] = $this->cssGenerator->generate('.cta-box-sidebar .btn-cta-box:hover', [
+ 'background-color' => $colors['button_hover_background'] ?? '#1e3a5f',
+ 'color' => $colors['button_hover_text_color'] ?? '#ffffff',
+ ]);
+
+ // Button icon spacing
+ $cssRules[] = $this->cssGenerator->generate('.cta-box-sidebar .btn-cta-box i', [
+ 'margin-right' => $spacing['icon_margin_right'] ?? '0.5rem',
+ ]);
+
+ // Responsive visibility
+ $showOnDesktop = $visibility['show_on_desktop'] ?? true;
+ $showOnMobile = $visibility['show_on_mobile'] ?? false;
+
+ if (!$showOnMobile) {
+ $cssRules[] = "@media (max-width: 991.98px) {
+ .cta-box-sidebar { display: none !important; }
+ }";
+ }
+
+ if (!$showOnDesktop) {
+ $cssRules[] = "@media (min-width: 992px) {
+ .cta-box-sidebar { display: none !important; }
+ }";
+ }
+
+ return implode("\n", $cssRules);
+ }
+
+ private function buildHTML(array $data): string
+ {
+ $content = $data['content'] ?? [];
+
+ $title = $content['title'] ?? '¿Listo para potenciar tus proyectos?';
+ $description = $content['description'] ?? 'Accede a nuestra biblioteca completa de APUs y herramientas profesionales.';
+ $buttonText = $content['button_text'] ?? 'Solicitar Demo';
+ $buttonIcon = $content['button_icon'] ?? 'bi bi-calendar-check';
+ $buttonAction = $content['button_action'] ?? 'modal';
+ $buttonLink = $content['button_link'] ?? '#contactModal';
+
+ // Build button attributes based on action type
+ $buttonAttributes = $this->getButtonAttributes($buttonAction, $buttonLink);
+
+ $html = '';
+
+ return $html;
+ }
+
+ private function getButtonAttributes(string $action, string $link): string
+ {
+ switch ($action) {
+ case 'modal':
+ // Extract modal ID from link (e.g., #contactModal -> contactModal)
+ $modalId = ltrim($link, '#');
+ return sprintf(
+ 'type="button" data-bs-toggle="modal" data-bs-target="#%s"',
+ esc_attr($modalId)
+ );
+
+ case 'link':
+ return sprintf(
+ 'type="button" data-cta-action="link" data-cta-href="%s"',
+ esc_url($link)
+ );
+
+ case 'scroll':
+ $targetId = ltrim($link, '#');
+ return sprintf(
+ 'type="button" data-cta-action="scroll" data-cta-target="%s"',
+ esc_attr($targetId)
+ );
+
+ default:
+ return 'type="button"';
+ }
+ }
+
+ private function getVisibilityClasses(bool $desktop, bool $mobile): ?string
+ {
+ if (!$desktop && !$mobile) {
+ return null;
+ }
+ if (!$desktop && $mobile) {
+ return 'd-lg-none';
+ }
+ if ($desktop && !$mobile) {
+ return 'd-none d-lg-block';
+ }
+ return '';
+ }
+
+ private function buildScript(): string
+ {
+ return <<
+document.addEventListener('DOMContentLoaded', function() {
+ var ctaButtons = document.querySelectorAll('.btn-cta-box[data-cta-action]');
+ ctaButtons.forEach(function(btn) {
+ btn.addEventListener('click', function() {
+ var action = this.getAttribute('data-cta-action');
+ if (action === 'link') {
+ var href = this.getAttribute('data-cta-href');
+ if (href) window.location.href = href;
+ } else if (action === 'scroll') {
+ var target = this.getAttribute('data-cta-target');
+ var el = document.getElementById(target);
+ if (el) el.scrollIntoView({behavior: 'smooth'});
+ }
+ });
+ });
+});
+
+JS;
+ }
+}
diff --git a/Public/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkRenderer.php b/Public/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkRenderer.php
new file mode 100644
index 00000000..34382d7e
--- /dev/null
+++ b/Public/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkRenderer.php
@@ -0,0 +1,360 @@
+getData();
+
+ // Validar visibilidad general
+ if (!$this->isEnabled($data)) {
+ return '';
+ }
+
+ // Validar visibilidad por página
+ if (!$this->shouldShowOnCurrentPage($data)) {
+ return '';
+ }
+
+ // Generar CSS usando CSSGeneratorService
+ $css = $this->generateCSS($data);
+
+ // Generar HTML
+ $html = $this->buildHTML($data);
+
+ // Combinar todo
+ return sprintf(
+ "\n%s",
+ $css,
+ $html
+ );
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function supports(string $componentType): bool
+ {
+ return $componentType === 'cta-lets-talk';
+ }
+
+ /**
+ * Verificar si el componente está habilitado
+ *
+ * @param array $data Datos del componente
+ * @return bool
+ */
+ private function isEnabled(array $data): bool
+ {
+ return ($data['visibility']['is_enabled'] ?? false) === true;
+ }
+
+ /**
+ * Verificar si debe mostrarse en la página actual
+ *
+ * @param array $data Datos del componente
+ * @return bool
+ */
+ private function shouldShowOnCurrentPage(array $data): bool
+ {
+ $showOn = $data['visibility']['show_on_pages'] ?? 'all';
+
+ return match ($showOn) {
+ 'all' => true,
+ 'home' => is_front_page(),
+ 'posts' => is_single(),
+ 'pages' => is_page(),
+ default => true,
+ };
+ }
+
+ /**
+ * Calcular clases de visibilidad responsive
+ *
+ * @param bool $desktop Mostrar en desktop
+ * @param bool $mobile Mostrar en mobile
+ * @return string|null Clases CSS o null si no debe mostrarse
+ */
+ private function getVisibilityClasses(bool $desktop, bool $mobile): ?string
+ {
+ if (!$desktop && !$mobile) {
+ return null;
+ }
+ if (!$desktop && $mobile) {
+ return 'd-lg-none';
+ }
+ if ($desktop && !$mobile) {
+ return 'd-none d-lg-block';
+ }
+ return '';
+ }
+
+ /**
+ * Generar CSS usando CSSGeneratorService
+ *
+ * @param array $data Datos del componente
+ * @return string CSS generado
+ */
+ private function generateCSS(array $data): string
+ {
+ $css = '';
+
+ // Estilos base del botón
+ $baseStyles = [
+ 'background_color' => $data['colors']['background_color'] ?? '#FF8600',
+ 'color' => $data['colors']['text_color'] ?? '#FFFFFF',
+ 'font_size' => $data['typography']['font_size'] ?? '1rem',
+ 'font_weight' => $data['typography']['font_weight'] ?? '600',
+ 'text_transform' => $data['typography']['text_transform'] ?? 'none',
+ 'padding' => sprintf(
+ '%s %s',
+ $data['spacing']['padding_top_bottom'] ?? '0.5rem',
+ $data['spacing']['padding_left_right'] ?? '1.5rem'
+ ),
+ 'border' => sprintf(
+ '%s solid %s',
+ $data['visual_effects']['border_width'] ?? '0',
+ $data['colors']['border_color'] ?? 'transparent'
+ ),
+ 'border_radius' => $data['visual_effects']['border_radius'] ?? '6px',
+ 'box_shadow' => $data['visual_effects']['box_shadow'] ?? 'none',
+ 'transition' => sprintf(
+ 'all %s ease',
+ $data['visual_effects']['transition_duration'] ?? '0.3s'
+ ),
+ 'cursor' => 'pointer',
+ ];
+ $css .= $this->cssGenerator->generate('.btn-lets-talk', $baseStyles);
+
+ // Estilos hover del botón
+ $hoverStyles = [
+ 'background_color' => $data['colors']['background_hover_color'] ?? '#FF6B35',
+ 'color' => $data['colors']['text_hover_color'] ?? '#FFFFFF',
+ ];
+ $css .= "\n" . $this->cssGenerator->generate('.btn-lets-talk:hover', $hoverStyles);
+
+ // Estilos del ícono dentro del botón
+ $iconStyles = [
+ 'color' => $data['colors']['text_color'] ?? '#FFFFFF',
+ 'margin_right' => $data['spacing']['icon_spacing'] ?? '0.5rem',
+ ];
+ $css .= "\n" . $this->cssGenerator->generate('.btn-lets-talk i', $iconStyles);
+
+ // Estilos responsive - ocultar en móvil si show_on_mobile = false
+ $showOnMobile = ($data['visibility']['show_on_mobile'] ?? false) === true;
+ if (!$showOnMobile) {
+ $responsiveStyles = [
+ 'display' => 'none !important',
+ ];
+ $css .= "\n@media (max-width: 991px) {\n";
+ $css .= $this->cssGenerator->generate('.btn-lets-talk', $responsiveStyles);
+ $css .= "\n}";
+ }
+
+ // Estilos responsive - ocultar en desktop si show_on_desktop = false
+ $showOnDesktop = ($data['visibility']['show_on_desktop'] ?? true) === true;
+ if (!$showOnDesktop) {
+ $responsiveStyles = [
+ 'display' => 'none !important',
+ ];
+ $css .= "\n@media (min-width: 992px) {\n";
+ $css .= $this->cssGenerator->generate('.btn-lets-talk', $responsiveStyles);
+ $css .= "\n}";
+ }
+
+ // Margen izquierdo para separar del menú (solo desktop)
+ $marginLeft = $data['spacing']['margin_left'] ?? '1rem';
+ if (!empty($marginLeft) && $marginLeft !== '0') {
+ $css .= "\n@media (min-width: 992px) {\n";
+ $css .= $this->cssGenerator->generate('.btn-lets-talk', ['margin_left' => $marginLeft]);
+ $css .= "\n}";
+ }
+
+ return $css;
+ }
+
+ /**
+ * Generar HTML del componente
+ *
+ * @param array $data Datos del componente
+ * @return string HTML generado
+ */
+ private function buildHTML(array $data): string
+ {
+ $classes = $this->buildClasses($data);
+ $attributes = $this->buildAttributes($data);
+ $content = $this->buildContent($data);
+
+ $tag = $this->useModal($data) ? 'button' : 'a';
+
+ return sprintf(
+ '<%s class="%s"%s>%s%s>',
+ $tag,
+ esc_attr($classes),
+ $attributes,
+ $content,
+ $tag
+ );
+ }
+
+ /**
+ * Construir clases CSS del componente
+ *
+ * @param array $data Datos del componente
+ * @return string Clases CSS
+ */
+ private function buildClasses(array $data): string
+ {
+ $classes = ['btn', 'btn-lets-talk'];
+
+ // Agregar clase ms-lg-3 para margen en desktop (Bootstrap)
+ // Esto solo aplica en pantallas >= lg (992px)
+ $classes[] = 'ms-lg-3';
+
+ return implode(' ', $classes);
+ }
+
+ /**
+ * Determinar si debe usar modal o URL
+ *
+ * @param array $data Datos del componente
+ * @return bool
+ */
+ private function useModal(array $data): bool
+ {
+ return ($data['behavior']['enable_modal'] ?? true) === true;
+ }
+
+ /**
+ * Construir atributos HTML del componente
+ *
+ * @param array $data Datos del componente
+ * @return string Atributos HTML
+ */
+ private function buildAttributes(array $data): string
+ {
+ $attributes = [];
+
+ if ($this->useModal($data)) {
+ // Atributos para modal de Bootstrap
+ $attributes[] = 'type="button"';
+ $attributes[] = 'data-bs-toggle="modal"';
+
+ $modalTarget = $data['content']['modal_target'] ?? '#contactModal';
+ $attributes[] = sprintf('data-bs-target="%s"', esc_attr($modalTarget));
+ } else {
+ // Atributos para enlace
+ $customUrl = $data['behavior']['custom_url'] ?? '';
+ $attributes[] = sprintf('href="%s"', esc_url($customUrl ?: '#'));
+
+ if (($data['behavior']['open_in_new_tab'] ?? false) === true) {
+ $attributes[] = 'target="_blank"';
+ $attributes[] = 'rel="noopener noreferrer"';
+ }
+ }
+
+ // Atributo ARIA para accesibilidad
+ $ariaLabel = $data['content']['aria_label'] ?? 'Abrir formulario de contacto';
+ if (!empty($ariaLabel)) {
+ $attributes[] = sprintf('aria-label="%s"', esc_attr($ariaLabel));
+ }
+
+ return !empty($attributes) ? ' ' . implode(' ', $attributes) : '';
+ }
+
+ /**
+ * Construir contenido del botón
+ *
+ * @param array $data Datos del componente
+ * @return string HTML del contenido
+ */
+ private function buildContent(array $data): string
+ {
+ $html = '';
+
+ // Ícono (si está habilitado)
+ if ($this->shouldShowIcon($data)) {
+ $html .= $this->buildIcon($data);
+ }
+
+ // Texto del botón
+ $buttonText = $data['content']['button_text'] ?? "Let's Talk";
+ $html .= esc_html($buttonText);
+
+ return $html;
+ }
+
+ /**
+ * Verificar si debe mostrar el ícono
+ *
+ * @param array $data Datos del componente
+ * @return bool
+ */
+ private function shouldShowIcon(array $data): bool
+ {
+ return ($data['content']['show_icon'] ?? true) === true;
+ }
+
+ /**
+ * Construir ícono del componente
+ *
+ * @param array $data Datos del componente
+ * @return string HTML del ícono
+ */
+ private function buildIcon(array $data): string
+ {
+ $iconClass = $data['content']['icon_class'] ?? 'bi-lightning-charge-fill';
+
+ // Asegurar prefijo 'bi-'
+ if (strpos($iconClass, 'bi-') !== 0) {
+ $iconClass = 'bi-' . $iconClass;
+ }
+
+ return sprintf(
+ ' ',
+ esc_attr($iconClass)
+ );
+ }
+}
diff --git a/Public/CtaPost/Infrastructure/Ui/CtaPostRenderer.php b/Public/CtaPost/Infrastructure/Ui/CtaPostRenderer.php
new file mode 100644
index 00000000..831cfb27
--- /dev/null
+++ b/Public/CtaPost/Infrastructure/Ui/CtaPostRenderer.php
@@ -0,0 +1,187 @@
+getData();
+
+ if (!$this->isEnabled($data)) {
+ return '';
+ }
+
+ if (!$this->shouldShowOnCurrentPage($data)) {
+ return '';
+ }
+
+ $css = $this->generateCSS($data);
+ $html = $this->buildHTML($data);
+
+ return sprintf("\n%s", $css, $html);
+ }
+
+ public function supports(string $componentType): bool
+ {
+ return $componentType === 'cta-post';
+ }
+
+ private function isEnabled(array $data): bool
+ {
+ $value = $data['visibility']['is_enabled'] ?? false;
+ return $value === true || $value === '1' || $value === 1;
+ }
+
+ private function shouldShowOnCurrentPage(array $data): bool
+ {
+ $showOn = $data['visibility']['show_on_pages'] ?? 'posts';
+
+ switch ($showOn) {
+ case 'all':
+ return true;
+ case 'posts':
+ return is_single();
+ case 'pages':
+ return is_page();
+ default:
+ return true;
+ }
+ }
+
+ private function generateCSS(array $data): string
+ {
+ $colors = $data['colors'] ?? [];
+ $effects = $data['visual_effects'] ?? [];
+ $visibility = $data['visibility'] ?? [];
+
+ $cssRules = [];
+
+ $gradientStart = $colors['gradient_start'] ?? '#FF8600';
+ $gradientEnd = $colors['gradient_end'] ?? '#FFB800';
+ $gradientAngle = $effects['gradient_angle'] ?? '135deg';
+ $buttonBgColor = $colors['button_bg_color'] ?? '#FF8600';
+ $buttonTextColor = $colors['button_text_color'] ?? '#ffffff';
+ $buttonHoverBgColor = $colors['button_hover_bg_color'] ?? '#e67a00';
+
+ // Container - gradient background
+ $cssRules[] = $this->cssGenerator->generate('.cta-post-container', [
+ 'background' => "linear-gradient({$gradientAngle}, {$gradientStart} 0%, {$gradientEnd} 100%)",
+ ]);
+
+ // Button styles (matching template .cta-button) - Using !important to override Bootstrap btn-light
+ $cssRules[] = ".cta-post-container .cta-button {
+ background-color: {$buttonBgColor} !important;
+ color: {$buttonTextColor} !important;
+ font-weight: 600;
+ padding: 0.75rem 2rem;
+ border: none !important;
+ border-radius: 8px;
+ transition: 0.3s;
+ text-decoration: none;
+ display: inline-block;
+ }";
+
+ // Button hover state
+ $cssRules[] = ".cta-post-container .cta-button:hover {
+ background-color: {$buttonHoverBgColor};
+ color: {$buttonTextColor};
+ }";
+
+ // Responsive: button full width on mobile
+ $cssRules[] = "@media (max-width: 768px) {
+ .cta-post-container .cta-button {
+ width: 100%;
+ margin-top: 1rem;
+ }
+ }";
+
+ // Responsive visibility
+ $showOnDesktop = $visibility['show_on_desktop'] ?? true;
+ $showOnDesktop = $showOnDesktop === true || $showOnDesktop === '1' || $showOnDesktop === 1;
+ $showOnMobile = $visibility['show_on_mobile'] ?? true;
+ $showOnMobile = $showOnMobile === true || $showOnMobile === '1' || $showOnMobile === 1;
+
+ if (!$showOnMobile) {
+ $cssRules[] = "@media (max-width: 991.98px) {
+ .cta-post-container { display: none !important; }
+ }";
+ }
+
+ if (!$showOnDesktop) {
+ $cssRules[] = "@media (min-width: 992px) {
+ .cta-post-container { display: none !important; }
+ }";
+ }
+
+ return implode("\n", $cssRules);
+ }
+
+ private function buildHTML(array $data): string
+ {
+ $content = $data['content'] ?? [];
+
+ $title = $content['title'] ?? 'Accede a 200,000+ Análisis de Precios Unitarios';
+ $description = $content['description'] ?? '';
+ $buttonText = $content['button_text'] ?? 'Ver Catálogo Completo';
+ $buttonUrl = $content['button_url'] ?? '#';
+ $buttonIcon = $content['button_icon'] ?? 'bi-arrow-right';
+
+ $html = '';
+ $html .= '
';
+
+ // Left column - Content
+ $html .= '
';
+ $html .= sprintf(
+ '
%s ',
+ esc_html($title)
+ );
+ if (!empty($description)) {
+ $html .= sprintf(
+ '
%s
',
+ esc_html($description)
+ );
+ }
+ $html .= '
';
+
+ // Right column - Button
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+}
diff --git a/Public/FeaturedImage/Infrastructure/Ui/FeaturedImageRenderer.php b/Public/FeaturedImage/Infrastructure/Ui/FeaturedImageRenderer.php
new file mode 100644
index 00000000..023fe981
--- /dev/null
+++ b/Public/FeaturedImage/Infrastructure/Ui/FeaturedImageRenderer.php
@@ -0,0 +1,202 @@
+getData();
+
+ if (!$this->isEnabled($data)) {
+ return '';
+ }
+
+ if (!$this->shouldShowOnCurrentPage($data)) {
+ return '';
+ }
+
+ if (!$this->hasPostThumbnail()) {
+ return '';
+ }
+
+ $css = $this->generateCSS($data);
+ $html = $this->buildHTML($data);
+
+ return sprintf("\n%s", $css, $html);
+ }
+
+ public function supports(string $componentType): bool
+ {
+ return $componentType === 'featured-image';
+ }
+
+ private function isEnabled(array $data): bool
+ {
+ return ($data['visibility']['is_enabled'] ?? false) === true;
+ }
+
+ private function shouldShowOnCurrentPage(array $data): bool
+ {
+ $showOn = $data['visibility']['show_on_pages'] ?? 'posts';
+
+ switch ($showOn) {
+ case 'all':
+ return true;
+ case 'posts':
+ return is_single();
+ case 'pages':
+ return is_page();
+ default:
+ return true;
+ }
+ }
+
+ private function hasPostThumbnail(): bool
+ {
+ return is_singular() && has_post_thumbnail();
+ }
+
+ private function generateCSS(array $data): string
+ {
+ $spacing = $data['spacing'] ?? [];
+ $effects = $data['visual_effects'] ?? [];
+ $visibility = $data['visibility'] ?? [];
+
+ $marginTop = $spacing['margin_top'] ?? '1rem';
+ $marginBottom = $spacing['margin_bottom'] ?? '2rem';
+
+ $borderRadius = $effects['border_radius'] ?? '12px';
+ $boxShadow = $effects['box_shadow'] ?? '0 8px 24px rgba(0, 0, 0, 0.1)';
+ $hoverEffect = $effects['hover_effect'] ?? true;
+ $hoverScale = $effects['hover_scale'] ?? '1.02';
+ $transitionDuration = $effects['transition_duration'] ?? '0.3s';
+
+ $showOnDesktop = $visibility['show_on_desktop'] ?? true;
+ $showOnMobile = $visibility['show_on_mobile'] ?? true;
+
+ $cssRules = [];
+
+ // Container styles
+ $cssRules[] = $this->cssGenerator->generate('.featured-image-container', [
+ 'border-radius' => $borderRadius,
+ 'overflow' => 'hidden',
+ 'box-shadow' => $boxShadow,
+ 'margin-top' => $marginTop,
+ 'margin-bottom' => $marginBottom,
+ 'transition' => "transform {$transitionDuration} ease, box-shadow {$transitionDuration} ease",
+ ]);
+
+ // Image styles
+ $cssRules[] = $this->cssGenerator->generate('.featured-image-container img', [
+ 'width' => '100%',
+ 'height' => 'auto',
+ 'display' => 'block',
+ 'transition' => "transform {$transitionDuration} ease",
+ ]);
+
+ // Hover effect
+ if ($hoverEffect) {
+ $cssRules[] = $this->cssGenerator->generate('.featured-image-container:hover', [
+ 'box-shadow' => '0 12px 32px rgba(0, 0, 0, 0.15)',
+ ]);
+
+ $cssRules[] = $this->cssGenerator->generate('.featured-image-container:hover img', [
+ 'transform' => "scale({$hoverScale})",
+ ]);
+ }
+
+ // Link styles (remove default link styling)
+ $cssRules[] = $this->cssGenerator->generate('.featured-image-container a', [
+ 'display' => 'block',
+ 'line-height' => '0',
+ ]);
+
+ // Responsive visibility
+ if (!$showOnMobile) {
+ $cssRules[] = "@media (max-width: 767.98px) {
+ .featured-image-container { display: none !important; }
+ }";
+ }
+
+ if (!$showOnDesktop) {
+ $cssRules[] = "@media (min-width: 768px) {
+ .featured-image-container { display: none !important; }
+ }";
+ }
+
+ return implode("\n", $cssRules);
+ }
+
+ private function buildHTML(array $data): string
+ {
+ $content = $data['content'] ?? [];
+
+ $imageSize = $content['image_size'] ?? 'roi-featured-large';
+ $lazyLoading = $content['lazy_loading'] ?? true;
+ $linkToMedia = $content['link_to_media'] ?? false;
+
+ $imgAttr = [
+ 'class' => 'img-fluid featured-image',
+ 'alt' => get_the_title(),
+ ];
+
+ if ($lazyLoading) {
+ $imgAttr['loading'] = 'lazy';
+ }
+
+ $thumbnail = get_the_post_thumbnail(null, $imageSize, $imgAttr);
+
+ if (empty($thumbnail)) {
+ return '';
+ }
+
+ $html = '';
+
+ return $html;
+ }
+}
diff --git a/Public/Footer/Infrastructure/Api/WordPress/NewsletterAjaxHandler.php b/Public/Footer/Infrastructure/Api/WordPress/NewsletterAjaxHandler.php
new file mode 100644
index 00000000..24a92bb1
--- /dev/null
+++ b/Public/Footer/Infrastructure/Api/WordPress/NewsletterAjaxHandler.php
@@ -0,0 +1,185 @@
+ __('Error de seguridad. Por favor recarga la pagina.', 'roi-theme')
+ ], 403);
+ return;
+ }
+
+ // 2. Rate limiting (1 suscripcion por IP cada 60 segundos)
+ if (!$this->checkRateLimit()) {
+ wp_send_json_error([
+ 'message' => __('Por favor espera un momento antes de intentar de nuevo.', 'roi-theme')
+ ], 429);
+ return;
+ }
+
+ // 3. Validar email
+ $email = sanitize_email($_POST['email'] ?? '');
+ if (empty($email) || !is_email($email)) {
+ wp_send_json_error([
+ 'message' => __('Por favor ingresa un email valido.', 'roi-theme')
+ ], 422);
+ return;
+ }
+
+ // 4. Obtener configuracion del componente
+ $settings = $this->settingsRepository->getComponentSettings(self::COMPONENT_NAME);
+
+ if (empty($settings)) {
+ wp_send_json_error([
+ 'message' => __('Error de configuracion. Contacta al administrador.', 'roi-theme')
+ ], 500);
+ return;
+ }
+
+ $newsletter = $settings['newsletter'] ?? [];
+ $webhookUrl = $newsletter['newsletter_webhook_url'] ?? '';
+ $successMsg = $newsletter['newsletter_success_message'] ?? __('Gracias por suscribirte!', 'roi-theme');
+ $errorMsg = $newsletter['newsletter_error_message'] ?? __('Error al suscribirse. Intenta de nuevo.', 'roi-theme');
+
+ if (empty($webhookUrl)) {
+ // Si no hay webhook, simular exito para UX pero loguear warning
+ error_log('ROI Theme Newsletter: No webhook URL configured');
+ wp_send_json_success([
+ 'message' => $successMsg
+ ]);
+ return;
+ }
+
+ // 5. Preparar payload
+ $payload = [
+ 'email' => $email,
+ 'source' => 'newsletter-footer',
+ 'timestamp' => current_time('c'),
+ 'siteName' => get_bloginfo('name'),
+ 'siteUrl' => home_url(),
+ ];
+
+ // 6. Enviar a webhook
+ $result = $this->sendToWebhook($webhookUrl, $payload);
+
+ if ($result['success']) {
+ wp_send_json_success([
+ 'message' => $successMsg
+ ]);
+ } else {
+ error_log('ROI Theme Newsletter webhook error: ' . $result['error']);
+ wp_send_json_error([
+ 'message' => $errorMsg
+ ], 500);
+ }
+ }
+
+ /**
+ * Enviar datos al webhook
+ */
+ private function sendToWebhook(string $url, array $payload): array
+ {
+ $response = wp_remote_post($url, [
+ 'timeout' => 30,
+ 'headers' => [
+ 'Content-Type' => 'application/json',
+ 'Accept' => 'application/json',
+ ],
+ 'body' => wp_json_encode($payload),
+ ]);
+
+ if (is_wp_error($response)) {
+ return [
+ 'success' => false,
+ 'error' => $response->get_error_message()
+ ];
+ }
+
+ $statusCode = wp_remote_retrieve_response_code($response);
+
+ if ($statusCode >= 200 && $statusCode < 300) {
+ return ['success' => true, 'error' => ''];
+ }
+
+ return [
+ 'success' => false,
+ 'error' => sprintf('HTTP %d: %s', $statusCode, wp_remote_retrieve_response_message($response))
+ ];
+ }
+
+ /**
+ * Rate limiting por IP
+ */
+ private function checkRateLimit(): bool
+ {
+ $ip = $this->getClientIP();
+ $transientKey = 'roi_newsletter_' . md5($ip);
+ $lastSubmit = get_transient($transientKey);
+
+ if ($lastSubmit !== false) {
+ return false;
+ }
+
+ set_transient($transientKey, time(), 60);
+ return true;
+ }
+
+ /**
+ * Obtener IP del cliente
+ */
+ private function getClientIP(): string
+ {
+ $ip = '';
+
+ if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
+ $ip = sanitize_text_field($_SERVER['HTTP_CLIENT_IP']);
+ } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
+ $ip = sanitize_text_field(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0]);
+ } elseif (!empty($_SERVER['REMOTE_ADDR'])) {
+ $ip = sanitize_text_field($_SERVER['REMOTE_ADDR']);
+ }
+
+ return $ip;
+ }
+}
diff --git a/Public/Footer/Infrastructure/Ui/FooterRenderer.php b/Public/Footer/Infrastructure/Ui/FooterRenderer.php
new file mode 100644
index 00000000..056b0990
--- /dev/null
+++ b/Public/Footer/Infrastructure/Ui/FooterRenderer.php
@@ -0,0 +1,423 @@
+getData();
+
+ // Validar visibilidad
+ $visibility = $data['visibility'] ?? [];
+ if (!($visibility['is_enabled'] ?? true)) {
+ return '';
+ }
+
+ // Verificar visibilidad responsive
+ $showDesktop = $visibility['show_on_desktop'] ?? true;
+ $showMobile = $visibility['show_on_mobile'] ?? true;
+
+ if (!$showDesktop && !$showMobile) {
+ return '';
+ }
+
+ // Generar CSS
+ $css = $this->generateCSS($data, $showDesktop, $showMobile);
+
+ // Generar HTML
+ $html = $this->generateHTML($data);
+
+ // Generar JavaScript
+ $js = $this->generateJS($data);
+
+ return $css . $html . $js;
+ }
+
+ private function generateCSS(array $data, bool $showDesktop, bool $showMobile): string
+ {
+ $colors = $data['colors'] ?? [];
+ $spacing = $data['spacing'] ?? [];
+ $effects = $data['visual_effects'] ?? [];
+
+ // Valores con fallbacks
+ $bgColor = $colors['bg_color'] ?? '#212529';
+ $textColor = $colors['text_color'] ?? '#ffffff';
+ $titleColor = $colors['title_color'] ?? '#ffffff';
+ $linkColor = $colors['link_color'] ?? '#ffffff';
+ $linkHoverColor = $colors['link_hover_color'] ?? '#FF8600';
+ $inputBgColor = $colors['input_bg_color'] ?? '#ffffff';
+ $inputTextColor = $colors['input_text_color'] ?? '#212529';
+ $inputBorderColor = $colors['input_border_color'] ?? '#dee2e6';
+ $buttonBgColor = $colors['button_bg_color'] ?? '#0d6efd';
+ $buttonTextColor = $colors['button_text_color'] ?? '#ffffff';
+ $buttonHoverBg = $colors['button_hover_bg'] ?? '#0b5ed7';
+ $borderTopColor = $colors['border_top_color'] ?? 'rgba(255, 255, 255, 0.2)';
+
+ $paddingY = $spacing['padding_y'] ?? '3rem';
+ $marginTop = $spacing['margin_top'] ?? '0';
+ $widgetTitleMb = $spacing['widget_title_margin_bottom'] ?? '1rem';
+ $linkMb = $spacing['link_margin_bottom'] ?? '0.5rem';
+ $copyrightPy = $spacing['copyright_padding_y'] ?? '1.5rem';
+
+ $inputRadius = $effects['input_border_radius'] ?? '6px';
+ $buttonRadius = $effects['button_border_radius'] ?? '6px';
+ $transition = $effects['transition_duration'] ?? '0.3s';
+
+ $cssRules = [];
+
+ // Footer principal
+ $cssRules[] = $this->cssGenerator->generate('.roi-footer', [
+ 'background-color' => $bgColor,
+ 'color' => $textColor,
+ 'padding-top' => $paddingY,
+ 'padding-bottom' => $paddingY,
+ 'margin-top' => $marginTop,
+ ]);
+
+ // Grid custom para 3+3+3+4 = 13 columnas
+ $cssRules[] = $this->cssGenerator->generate('.roi-footer .footer-grid', [
+ 'display' => 'grid',
+ 'grid-template-columns' => 'repeat(4, 1fr)',
+ 'gap' => '2rem',
+ ]);
+
+ // En desktop: distribucion 3+3+3+4
+ $cssRules[] = "@media (min-width: 768px) {
+ .roi-footer .footer-grid {
+ grid-template-columns: 23% 23% 23% 31%;
+ }
+ }";
+
+ // En mobile: 2 columnas
+ $cssRules[] = "@media (max-width: 767px) {
+ .roi-footer .footer-grid {
+ grid-template-columns: 1fr 1fr;
+ }
+ .roi-footer .footer-widget-newsletter {
+ grid-column: span 2;
+ }
+ }";
+
+ // Titulos de widgets
+ $cssRules[] = $this->cssGenerator->generate('.roi-footer .widget-title', [
+ 'color' => $titleColor,
+ 'font-size' => '1.25rem',
+ 'font-weight' => '500',
+ 'margin-bottom' => $widgetTitleMb,
+ ]);
+
+ // Links de navegacion
+ $cssRules[] = $this->cssGenerator->generate('.roi-footer .footer-nav', [
+ 'list-style' => 'none',
+ 'padding' => '0',
+ 'margin' => '0',
+ ]);
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-footer .footer-nav li', [
+ 'margin-bottom' => $linkMb,
+ ]);
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-footer .footer-nav a', [
+ 'color' => $linkColor,
+ 'text-decoration' => 'none',
+ 'transition' => "color {$transition}",
+ ]);
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-footer .footer-nav a:hover', [
+ 'color' => $linkHoverColor,
+ ]);
+
+ // Newsletter description
+ $cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-description', [
+ 'color' => $textColor,
+ 'margin-bottom' => '1rem',
+ 'opacity' => '0.9',
+ ]);
+
+ // Input newsletter
+ $cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-input', [
+ 'width' => '100%',
+ 'padding' => '0.75rem 1rem',
+ 'background-color' => $inputBgColor,
+ 'color' => $inputTextColor,
+ 'border' => "1px solid {$inputBorderColor}",
+ 'border-radius' => $inputRadius,
+ 'margin-bottom' => '0.75rem',
+ ]);
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-input:focus', [
+ 'outline' => 'none',
+ 'border-color' => $buttonBgColor,
+ 'box-shadow' => "0 0 0 0.2rem rgba(13, 110, 253, 0.25)",
+ ]);
+
+ // Boton newsletter
+ $cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-btn', [
+ 'width' => '100%',
+ 'padding' => '0.75rem 1.5rem',
+ 'background-color' => $buttonBgColor,
+ 'color' => $buttonTextColor,
+ 'border' => 'none',
+ 'border-radius' => $buttonRadius,
+ 'font-weight' => '500',
+ 'cursor' => 'pointer',
+ 'transition' => "background-color {$transition}",
+ ]);
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-btn:hover', [
+ 'background-color' => $buttonHoverBg,
+ ]);
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-btn:disabled', [
+ 'opacity' => '0.7',
+ 'cursor' => 'not-allowed',
+ ]);
+
+ // Mensaje newsletter
+ $cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-message', [
+ 'margin-top' => '0.75rem',
+ 'padding' => '0.5rem',
+ 'border-radius' => '4px',
+ 'font-size' => '0.875rem',
+ 'display' => 'none',
+ ]);
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-message.success', [
+ 'background-color' => '#d1e7dd',
+ 'color' => '#0f5132',
+ ]);
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-footer .newsletter-message.error', [
+ 'background-color' => '#f8d7da',
+ 'color' => '#842029',
+ ]);
+
+ // Footer bottom (copyright)
+ $cssRules[] = $this->cssGenerator->generate('.roi-footer .footer-bottom', [
+ 'border-top' => "1px solid {$borderTopColor}",
+ 'padding-top' => $copyrightPy,
+ 'margin-top' => '2rem',
+ 'text-align' => 'center',
+ ]);
+
+ $cssRules[] = $this->cssGenerator->generate('.roi-footer .copyright-text', [
+ 'margin' => '0',
+ 'opacity' => '0.9',
+ ]);
+
+ // Responsive visibility
+ if (!$showDesktop) {
+ $cssRules[] = "@media (min-width: 992px) { .roi-footer { display: none !important; } }";
+ }
+ if (!$showMobile) {
+ $cssRules[] = "@media (max-width: 991px) { .roi-footer { display: none !important; } }";
+ }
+
+ return '';
+ }
+
+ private function generateHTML(array $data): string
+ {
+ $widget1 = $data['widget_1'] ?? [];
+ $widget2 = $data['widget_2'] ?? [];
+ $widget3 = $data['widget_3'] ?? [];
+ $newsletter = $data['newsletter'] ?? [];
+ $footerBottom = $data['footer_bottom'] ?? [];
+
+ $widget1Visible = $this->toBool($widget1['widget_1_visible'] ?? true);
+ $widget2Visible = $this->toBool($widget2['widget_2_visible'] ?? true);
+ $widget3Visible = $this->toBool($widget3['widget_3_visible'] ?? true);
+ $newsletterVisible = $this->toBool($newsletter['newsletter_visible'] ?? true);
+
+ $widget1Title = esc_html($widget1['widget_1_title'] ?? 'Recursos');
+ $widget2Title = esc_html($widget2['widget_2_title'] ?? 'Soporte');
+ $widget3Title = esc_html($widget3['widget_3_title'] ?? 'Empresa');
+
+ $newsletterTitle = esc_html($newsletter['newsletter_title'] ?? 'Suscribete al Newsletter');
+ $newsletterDesc = esc_html($newsletter['newsletter_description'] ?? 'Recibe las ultimas actualizaciones.');
+ $newsletterPlaceholder = esc_attr($newsletter['newsletter_placeholder'] ?? 'Email');
+ $newsletterBtnText = esc_html($newsletter['newsletter_button_text'] ?? 'Suscribirse');
+
+ $copyrightText = esc_html($footerBottom['copyright_text'] ?? date('Y') . ' Todos los derechos reservados.');
+
+ $nonce = wp_create_nonce(self::NONCE_ACTION);
+ $ajaxUrl = admin_url('admin-ajax.php');
+
+ $html = '';
+
+ return $html;
+ }
+
+ private function renderMenu(string $menuLocation): string
+ {
+ if (!has_nav_menu($menuLocation)) {
+ return 'Menu no asignado
';
+ }
+
+ return wp_nav_menu([
+ 'theme_location' => $menuLocation,
+ 'container' => false,
+ 'menu_class' => 'footer-nav',
+ 'fallback_cb' => false,
+ 'echo' => false,
+ 'depth' => 1,
+ ]) ?: '';
+ }
+
+ private function generateJS(array $data): string
+ {
+ $newsletter = $data['newsletter'] ?? [];
+ $successMsg = esc_js($newsletter['newsletter_success_message'] ?? 'Gracias por suscribirte!');
+ $errorMsg = esc_js($newsletter['newsletter_error_message'] ?? 'Error al suscribirse. Intenta de nuevo.');
+
+ $ajaxUrl = admin_url('admin-ajax.php');
+
+ $js = <<
+(function() {
+ const form = document.getElementById('roi-newsletter-form');
+ if (!form) return;
+
+ form.addEventListener('submit', async function(e) {
+ e.preventDefault();
+
+ const btn = form.querySelector('.newsletter-btn');
+ const msgDiv = form.querySelector('.newsletter-message');
+ const emailInput = form.querySelector('input[name="email"]');
+ const originalText = btn.textContent;
+
+ // Reset message
+ msgDiv.style.display = 'none';
+ msgDiv.className = 'newsletter-message';
+
+ // Validate email
+ if (!emailInput.value || !emailInput.validity.valid) {
+ msgDiv.textContent = 'Por favor ingresa un email valido';
+ msgDiv.classList.add('error');
+ msgDiv.style.display = 'block';
+ return;
+ }
+
+ // Disable button
+ btn.disabled = true;
+ btn.textContent = 'Enviando...';
+
+ try {
+ const formData = new FormData(form);
+
+ const response = await fetch('{$ajaxUrl}', {
+ method: 'POST',
+ body: formData
+ });
+
+ const result = await response.json();
+
+ if (result.success) {
+ msgDiv.textContent = '{$successMsg}';
+ msgDiv.classList.add('success');
+ emailInput.value = '';
+ } else {
+ msgDiv.textContent = result.data?.message || '{$errorMsg}';
+ msgDiv.classList.add('error');
+ }
+ } catch (error) {
+ msgDiv.textContent = '{$errorMsg}';
+ msgDiv.classList.add('error');
+ }
+
+ msgDiv.style.display = 'block';
+ btn.disabled = false;
+ btn.textContent = originalText;
+ });
+})();
+
+JS;
+
+ return $js;
+ }
+
+ private function toBool($value): bool
+ {
+ return $value === true || $value === '1' || $value === 1;
+ }
+}
diff --git a/Public/Hero/Infrastructure/Ui/HeroRenderer.php b/Public/Hero/Infrastructure/Ui/HeroRenderer.php
new file mode 100644
index 00000000..db8416e1
--- /dev/null
+++ b/Public/Hero/Infrastructure/Ui/HeroRenderer.php
@@ -0,0 +1,278 @@
+getData();
+
+ if (!$this->isEnabled($data)) {
+ return '';
+ }
+
+ if (!$this->shouldShowOnCurrentPage($data)) {
+ return '';
+ }
+
+ $css = $this->generateCSS($data);
+ $html = $this->buildHTML($data);
+
+ return sprintf("\n%s", $css, $html);
+ }
+
+ public function supports(string $componentType): bool
+ {
+ return $componentType === 'hero';
+ }
+
+ private function isEnabled(array $data): bool
+ {
+ return ($data['visibility']['is_enabled'] ?? false) === true;
+ }
+
+ private function shouldShowOnCurrentPage(array $data): bool
+ {
+ $showOn = $data['visibility']['show_on_pages'] ?? 'posts';
+
+ switch ($showOn) {
+ case 'all':
+ return true;
+ case 'home':
+ return is_front_page() || is_home();
+ case 'posts':
+ return is_single();
+ case 'pages':
+ return is_page();
+ default:
+ return true;
+ }
+ }
+
+ private function generateCSS(array $data): string
+ {
+ $colors = $data['colors'] ?? [];
+ $typography = $data['typography'] ?? [];
+ $spacing = $data['spacing'] ?? [];
+ $effects = $data['visual_effects'] ?? [];
+ $visibility = $data['visibility'] ?? [];
+
+ $gradientStart = $colors['gradient_start'] ?? '#1e3a5f';
+ $gradientEnd = $colors['gradient_end'] ?? '#2c5282';
+ $titleColor = $colors['title_color'] ?? '#FFFFFF';
+ $badgeBgColor = $colors['badge_bg_color'] ?? '#FFFFFF';
+ $badgeTextColor = $colors['badge_text_color'] ?? '#FFFFFF';
+ $badgeIconColor = $colors['badge_icon_color'] ?? '#FFB800';
+ $badgeHoverBg = $colors['badge_hover_bg'] ?? '#FF8600';
+
+ $titleFontSize = $typography['title_font_size'] ?? '2.5rem';
+ $titleFontSizeMobile = $typography['title_font_size_mobile'] ?? '1.75rem';
+ $titleFontWeight = $typography['title_font_weight'] ?? '700';
+ $titleLineHeight = $typography['title_line_height'] ?? '1.4';
+ $badgeFontSize = $typography['badge_font_size'] ?? '0.813rem';
+
+ $paddingVertical = $spacing['padding_vertical'] ?? '3rem';
+ $marginBottom = $spacing['margin_bottom'] ?? '1.5rem';
+ $badgePadding = $spacing['badge_padding'] ?? '0.375rem 0.875rem';
+ $badgeBorderRadius = $spacing['badge_border_radius'] ?? '20px';
+
+ $boxShadow = $effects['box_shadow'] ?? '0 4px 16px rgba(30, 58, 95, 0.25)';
+ $titleTextShadow = $effects['title_text_shadow'] ?? '1px 1px 2px rgba(0, 0, 0, 0.2)';
+ $badgeBackdropBlur = $effects['badge_backdrop_blur'] ?? '10px';
+
+ $showOnDesktop = $visibility['show_on_desktop'] ?? true;
+ $showOnMobile = $visibility['show_on_mobile'] ?? true;
+
+ $cssRules = [];
+
+ $cssRules[] = $this->cssGenerator->generate('.hero-section', [
+ 'background' => "linear-gradient(135deg, {$gradientStart} 0%, {$gradientEnd} 100%)",
+ 'box-shadow' => $boxShadow,
+ 'padding' => "{$paddingVertical} 0",
+ 'margin-bottom' => $marginBottom,
+ ]);
+
+ $cssRules[] = $this->cssGenerator->generate('.hero-section__title', [
+ 'color' => "{$titleColor} !important",
+ 'font-weight' => $titleFontWeight,
+ 'font-size' => $titleFontSize,
+ 'line-height' => $titleLineHeight,
+ 'text-shadow' => $titleTextShadow,
+ 'margin-bottom' => '0',
+ 'text-align' => 'center',
+ ]);
+
+ $cssRules[] = $this->cssGenerator->generate('.hero-section__badge', [
+ 'background' => $this->hexToRgba($badgeBgColor, 0.15),
+ 'backdrop-filter' => "blur({$badgeBackdropBlur})",
+ '-webkit-backdrop-filter' => "blur({$badgeBackdropBlur})",
+ 'border' => '1px solid ' . $this->hexToRgba($badgeBgColor, 0.2),
+ 'color' => $this->hexToRgba($badgeTextColor, 0.95),
+ 'padding' => $badgePadding,
+ 'border-radius' => $badgeBorderRadius,
+ 'font-size' => $badgeFontSize,
+ 'font-weight' => '500',
+ 'text-decoration' => 'none',
+ 'display' => 'inline-block',
+ 'transition' => 'all 0.3s ease',
+ ]);
+
+ $cssRules[] = $this->cssGenerator->generate('.hero-section__badge:hover', [
+ 'background' => $this->hexToRgba($badgeHoverBg, 0.2),
+ 'border-color' => $this->hexToRgba($badgeHoverBg, 0.4),
+ 'color' => '#ffffff',
+ ]);
+
+ $cssRules[] = $this->cssGenerator->generate('.hero-section__badge i', [
+ 'color' => $badgeIconColor,
+ ]);
+
+ $cssRules[] = "@media (max-width: 767.98px) {
+ .hero-section__title {
+ font-size: {$titleFontSizeMobile};
+ }
+ }";
+
+ if (!$showOnMobile) {
+ $cssRules[] = "@media (max-width: 767.98px) {
+ .hero-section { display: none !important; }
+ }";
+ }
+
+ if (!$showOnDesktop) {
+ $cssRules[] = "@media (min-width: 768px) {
+ .hero-section { display: none !important; }
+ }";
+ }
+
+ return implode("\n", $cssRules);
+ }
+
+ private function buildHTML(array $data): string
+ {
+ $content = $data['content'] ?? [];
+ $showCategories = $content['show_categories'] ?? true;
+ $showBadgeIcon = $content['show_badge_icon'] ?? true;
+ $badgeIconClass = $content['badge_icon_class'] ?? 'bi-folder-fill';
+ $titleTag = $content['title_tag'] ?? 'h1';
+
+ $allowedTags = ['h1', 'h2', 'div'];
+ if (!in_array($titleTag, $allowedTags, true)) {
+ $titleTag = 'h1';
+ }
+
+ $title = is_singular() ? get_the_title() : '';
+ if (empty($title)) {
+ $title = wp_title('', false);
+ }
+
+ $html = '';
+ $html .= '
';
+
+ if ($showCategories && is_single()) {
+ $categories = get_the_category();
+ if (!empty($categories)) {
+ $html .= '
';
+ $html .= '
';
+
+ foreach ($categories as $category) {
+ $categoryLink = esc_url(get_category_link($category->term_id));
+ $categoryName = esc_html($category->name);
+ $iconHtml = $showBadgeIcon
+ ? '
'
+ : '';
+
+ $html .= sprintf(
+ '
%s%s ',
+ $categoryLink,
+ $iconHtml,
+ $categoryName
+ );
+ }
+
+ $html .= '
';
+ $html .= '
';
+ }
+ }
+
+ $html .= sprintf(
+ '<%s class="hero-section__title">%s%s>',
+ $titleTag,
+ esc_html($title),
+ $titleTag
+ );
+
+ $html .= '
';
+ $html .= '
';
+
+ return $html;
+ }
+
+ private function hexToRgba(string $hex, float $alpha): string
+ {
+ $hex = ltrim($hex, '#');
+
+ if (strlen($hex) === 3) {
+ $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
+ }
+
+ $r = hexdec(substr($hex, 0, 2));
+ $g = hexdec(substr($hex, 2, 2));
+ $b = hexdec(substr($hex, 4, 2));
+
+ return "rgba({$r}, {$g}, {$b}, {$alpha})";
+ }
+
+ /**
+ * Genera clases Bootstrap de visibilidad responsive
+ *
+ * @param bool $desktop Si debe mostrarse en desktop
+ * @param bool $mobile Si debe mostrarse en mobile
+ * @return string|null Clases Bootstrap o null si visible en todos
+ */
+ private function getVisibilityClasses(bool $desktop, bool $mobile): ?string
+ {
+ if ($desktop && $mobile) {
+ return null;
+ }
+
+ if ($desktop && !$mobile) {
+ return 'd-none d-md-block';
+ }
+
+ if (!$desktop && $mobile) {
+ return 'd-block d-md-none';
+ }
+
+ return 'd-none';
+ }
+}
diff --git a/src/HeroSection/Infrastructure/Presentation/Public/HeroSectionRenderer.php b/Public/HeroSection/Infrastructure/Ui/HeroSectionRenderer.php
similarity index 95%
rename from src/HeroSection/Infrastructure/Presentation/Public/HeroSectionRenderer.php
rename to Public/HeroSection/Infrastructure/Ui/HeroSectionRenderer.php
index a8a79258..17960a89 100644
--- a/src/HeroSection/Infrastructure/Presentation/Public/HeroSectionRenderer.php
+++ b/Public/HeroSection/Infrastructure/Ui/HeroSectionRenderer.php
@@ -1,11 +1,24 @@
getData();
+
+ if (!$this->isEnabled($data)) {
+ return '';
+ }
+
+ $css = $this->generateCSS($data);
+ $html = $this->buildMenu($data);
+
+ return sprintf(
+ "\n%s",
+ $css,
+ $html
+ );
+ }
+
+ private function isEnabled(array $data): bool
+ {
+ return isset($data['visibility']['is_enabled']) &&
+ $data['visibility']['is_enabled'] === true;
+ }
+
+ private function shouldShowOnMobile(array $data): bool
+ {
+ return isset($data['visibility']['show_on_mobile']) &&
+ $data['visibility']['show_on_mobile'] === true;
+ }
+
+ /**
+ * Generar CSS usando CSSGeneratorService
+ *
+ * @param array $data Datos del componente
+ * @return string CSS generado
+ */
+ private function generateCSS(array $data): string
+ {
+ $css = '';
+
+ // Obtener valores de configuración
+ $stickyEnabled = $data['visibility']['sticky_enabled'] ?? true;
+ $paddingVertical = $data['layout']['padding_vertical'] ?? '0.75rem 0';
+ $zIndex = $data['layout']['z_index'] ?? '1030';
+
+ $bgColor = $data['colors']['background_color'] ?? '#1e3a5f';
+ $boxShadow = $data['colors']['box_shadow'] ?? '0 4px 12px rgba(30, 58, 95, 0.15)';
+
+ $linkTextColor = $data['links']['text_color'] ?? '#FFFFFF';
+ $linkHoverColor = $data['links']['hover_color'] ?? '#FF8600';
+ $linkActiveColor = $data['links']['active_color'] ?? '#FF8600';
+ $linkFontSize = $data['links']['font_size'] ?? '0.9rem';
+ $linkFontWeight = $data['links']['font_weight'] ?? '500';
+ $linkPadding = $data['links']['padding'] ?? '0.5rem 0.65rem';
+ $linkBorderRadius = $data['links']['border_radius'] ?? '4px';
+ $showUnderlineEffect = $data['links']['show_underline_effect'] ?? true;
+ $underlineColor = $data['links']['underline_color'] ?? '#FF8600';
+
+ // Estilos del navbar container
+ $navbarStyles = [
+ 'background-color' => $bgColor . ' !important',
+ 'box-shadow' => $boxShadow,
+ 'padding' => $paddingVertical,
+ 'transition' => 'all 0.3s ease',
+ ];
+
+ if ($stickyEnabled) {
+ $navbarStyles['position'] = 'sticky';
+ $navbarStyles['top'] = '0';
+ $navbarStyles['z-index'] = $zIndex;
+ }
+
+ $css .= $this->cssGenerator->generate('.navbar', $navbarStyles);
+
+ // Efecto scrolled del navbar
+ $css .= "\n" . $this->cssGenerator->generate('.navbar.scrolled', [
+ 'box-shadow' => '0 6px 20px rgba(30, 58, 95, 0.25)',
+ ]);
+
+ // Estilos de los enlaces del navbar
+ $navLinkStyles = [
+ 'color' => 'rgba(255, 255, 255, 0.9) !important',
+ 'font-weight' => $linkFontWeight,
+ 'position' => 'relative',
+ 'padding' => $linkPadding . ' !important',
+ 'transition' => 'all 0.3s ease',
+ 'font-size' => $linkFontSize,
+ 'white-space' => 'nowrap',
+ ];
+ $css .= "\n" . $this->cssGenerator->generate('.navbar .nav-link', $navLinkStyles);
+
+ // Efecto de subrayado (::after pseudo-element)
+ if ($showUnderlineEffect) {
+ $css .= "\n.navbar .nav-link::after {";
+ $css .= "\n content: '';";
+ $css .= "\n position: absolute;";
+ $css .= "\n bottom: 0;";
+ $css .= "\n left: 50%;";
+ $css .= "\n transform: translateX(-50%) scaleX(0);";
+ $css .= "\n width: 80%;";
+ $css .= "\n height: 2px;";
+ $css .= "\n background: {$underlineColor};";
+ $css .= "\n transition: transform 0.3s ease;";
+ $css .= "\n}";
+
+ $css .= "\n.navbar .nav-link:hover::after {";
+ $css .= "\n transform: translateX(-50%) scaleX(1);";
+ $css .= "\n}";
+ }
+
+ // Estilos hover y focus de los enlaces
+ $navLinkHoverStyles = [
+ 'color' => $linkHoverColor . ' !important',
+ 'background-color' => 'rgba(255, 133, 0, 0.1)',
+ 'border-radius' => $linkBorderRadius,
+ ];
+ $css .= "\n" . $this->cssGenerator->generate('.navbar .nav-link:hover, .navbar .nav-link:focus', $navLinkHoverStyles);
+
+ // Estilos de enlaces activos
+ $navLinkActiveStyles = [
+ 'color' => $linkActiveColor . ' !important',
+ ];
+ $css .= "\n" . $this->cssGenerator->generate('.navbar .nav-link.active, .navbar .nav-item.current-menu-item > .nav-link', $navLinkActiveStyles);
+
+ // Estilos del dropdown menu
+ $dropdownMaxHeight = $data['visual_effects']['dropdown_max_height'] ?? '300px';
+ $dropdownStyles = [
+ 'background' => $data['visual_effects']['background_color'] ?? '#ffffff',
+ 'border' => 'none',
+ 'box-shadow' => $data['visual_effects']['shadow'] ?? '0 8px 24px rgba(0, 0, 0, 0.12)',
+ 'border-radius' => $data['visual_effects']['border_radius'] ?? '8px',
+ 'padding' => '0.5rem 0',
+ 'max-height' => $dropdownMaxHeight,
+ 'overflow-y' => 'auto',
+ ];
+ $css .= "\n" . $this->cssGenerator->generate('.navbar .dropdown-menu', $dropdownStyles);
+
+ // Hover en desktop para mostrar dropdown (sin necesidad de clic)
+ $css .= "\n@media (min-width: 992px) {";
+ $css .= "\n .navbar .dropdown:hover > .dropdown-menu {";
+ $css .= "\n display: block;";
+ $css .= "\n margin-top: 0;";
+ $css .= "\n }";
+ $css .= "\n .navbar .dropdown > .dropdown-toggle:active {";
+ $css .= "\n pointer-events: none;";
+ $css .= "\n }";
+ $css .= "\n}";
+
+ // Estilos de items del dropdown
+ $dropdownItemStyles = [
+ 'color' => $data['visual_effects']['item_color'] ?? '#495057',
+ 'padding' => $data['visual_effects']['item_padding'] ?? '0.625rem 1.25rem',
+ 'transition' => 'all 0.3s ease',
+ 'font-weight' => '500',
+ ];
+ $css .= "\n" . $this->cssGenerator->generate('.navbar .dropdown-item', $dropdownItemStyles);
+
+ // Estilos hover de items del dropdown
+ $dropdownItemHoverStyles = [
+ 'background-color' => $data['visual_effects']['item_hover_background'] ?? 'rgba(255, 133, 0, 0.1)',
+ 'color' => $linkHoverColor,
+ ];
+ $css .= "\n" . $this->cssGenerator->generate('.navbar .dropdown-item:hover, .navbar .dropdown-item:focus', $dropdownItemHoverStyles);
+
+ // Estilos del brand (texto)
+ $brandStyles = [
+ 'color' => ($data['media']['brand_color'] ?? '#FFFFFF') . ' !important',
+ 'font-weight' => '700',
+ 'font-size' => $data['media']['brand_font_size'] ?? '1.5rem',
+ 'transition' => 'color 0.3s ease',
+ ];
+ $css .= "\n" . $this->cssGenerator->generate('.navbar .navbar-brand, .navbar .roi-navbar-brand', $brandStyles);
+
+ // Estilos hover del brand
+ $brandHoverStyles = [
+ 'color' => ($data['media']['brand_hover_color'] ?? '#FF8600') . ' !important',
+ ];
+ $css .= "\n" . $this->cssGenerator->generate('.navbar .navbar-brand:hover, .navbar .roi-navbar-brand:hover', $brandHoverStyles);
+
+ // Estilos del logo (imagen)
+ $logoStyles = [
+ 'height' => $data['media']['logo_height'] ?? '40px',
+ 'width' => 'auto',
+ ];
+ $css .= "\n" . $this->cssGenerator->generate('.navbar .roi-navbar-logo', $logoStyles);
+
+ return $css;
+ }
+
+ private function buildMenu(array $data): string
+ {
+ $menuLocation = $data['behavior']['menu_location'] ?? 'primary';
+ $enableDropdowns = $data['behavior']['enable_dropdowns'] ?? true;
+ $mobileBreakpoint = $data['behavior']['mobile_breakpoint'] ?? 'lg';
+
+ $ulClass = 'navbar-nav mb-2 mb-lg-0';
+
+ $args = [
+ 'theme_location' => $menuLocation === 'custom' ? '' : $menuLocation,
+ 'menu' => $menuLocation === 'custom' ? ($data['behavior']['custom_menu_id'] ?? 0) : '',
+ 'container' => false,
+ 'menu_class' => $ulClass,
+ 'fallback_cb' => '__return_false',
+ 'items_wrap' => '',
+ 'depth' => $enableDropdowns ? 2 : 1,
+ 'walker' => new ROI_Bootstrap_Nav_Walker()
+ ];
+
+ ob_start();
+ wp_nav_menu($args);
+ return ob_get_clean();
+ }
+
+ /**
+ * Obtiene las clases CSS de Bootstrap para visibilidad responsive
+ *
+ * Implementa tabla de decisión según especificación:
+ * - Desktop Y Mobile = null (visible en ambos)
+ * - Solo Desktop = 'd-none d-lg-block'
+ * - Solo Mobile = 'd-lg-none'
+ * - Ninguno = 'd-none' (oculto)
+ *
+ * @param bool $desktop Mostrar en desktop
+ * @param bool $mobile Mostrar en mobile
+ * @return string|null Clases CSS o null si visible en ambos
+ */
+ private function getVisibilityClasses(bool $desktop, bool $mobile): ?string
+ {
+ if ($desktop && $mobile) {
+ return null; // Sin clases = visible siempre
+ }
+ if ($desktop && !$mobile) {
+ return 'd-none d-lg-block';
+ }
+ if (!$desktop && $mobile) {
+ return 'd-lg-none';
+ }
+ return 'd-none';
+ }
+
+ public function supports(string $componentType): bool
+ {
+ return $componentType === 'navbar';
+ }
+}
+
+/**
+ * Custom Walker for Bootstrap 5 Navigation
+ *
+ * RESPONSABILIDAD: Adaptar wp_nav_menu() a Bootstrap 5
+ *
+ * CARACTERÍSTICAS:
+ * - Clases Bootstrap 5 (.nav-item, .nav-link, .dropdown)
+ * - Atributos data-bs-toggle para dropdowns
+ * - Soporte para current-menu-item
+ */
+class ROI_Bootstrap_Nav_Walker extends Walker_Nav_Menu
+{
+ public function start_lvl(&$output, $depth = 0, $args = null)
+ {
+ $indent = str_repeat("\t", $depth);
+ $output .= "\n$indent ';
- }
-}
-
-/**
- * EXAMPLE 10: Custom CSS in header
- */
-function example_add_custom_css() {
- $custom_css = roi_get_custom_css();
-
- if ($custom_css) {
- echo '\n";
- }
-}
-add_action('wp_head', 'example_add_custom_css', 100);
-
-/**
- * EXAMPLE 11: Custom JS in header
- */
-function example_add_custom_js_header() {
- $custom_js = roi_get_custom_js_header();
-
- if ($custom_js) {
- echo '\n";
- }
-}
-add_action('wp_head', 'example_add_custom_js_header', 100);
-
-/**
- * EXAMPLE 12: Custom JS in footer
- */
-function example_add_custom_js_footer() {
- $custom_js = roi_get_custom_js_footer();
-
- if ($custom_js) {
- echo '\n";
- }
-}
-add_action('wp_footer', 'example_add_custom_js_footer', 100);
-
-/**
- * EXAMPLE 13: Posts per page for archives
- */
-function example_set_archive_posts_per_page($query) {
- if ($query->is_archive() && !is_admin() && $query->is_main_query()) {
- $posts_per_page = roi_get_archive_posts_per_page();
- $query->set('posts_per_page', $posts_per_page);
- }
-}
-add_action('pre_get_posts', 'example_set_archive_posts_per_page');
-
-/**
- * EXAMPLE 14: Performance optimizations
- */
-function example_apply_performance_settings() {
- // Remove emoji scripts
- if (roi_is_performance_enabled('remove_emoji')) {
- remove_action('wp_head', 'print_emoji_detection_script', 7);
- remove_action('wp_print_styles', 'print_emoji_styles');
- }
-
- // Remove embeds
- if (roi_is_performance_enabled('remove_embeds')) {
- wp_deregister_script('wp-embed');
- }
-
- // Remove Dashicons for non-logged users
- if (roi_is_performance_enabled('remove_dashicons') && !is_user_logged_in()) {
- wp_deregister_style('dashicons');
- }
-}
-add_action('wp_enqueue_scripts', 'example_apply_performance_settings', 100);
-
-/**
- * EXAMPLE 15: Lazy loading images
- */
-function example_add_lazy_loading($attr, $attachment, $size) {
- if (roi_is_lazy_loading_enabled()) {
- $attr['loading'] = 'lazy';
- }
- return $attr;
-}
-add_filter('wp_get_attachment_image_attributes', 'example_add_lazy_loading', 10, 3);
-
-/**
- * EXAMPLE 16: Layout classes based on settings
- */
-function example_get_layout_class() {
- $layout = 'right-sidebar'; // default
-
- if (is_single()) {
- $layout = roi_get_default_post_layout();
- } elseif (is_page()) {
- $layout = roi_get_default_page_layout();
- }
-
- return 'layout-' . $layout;
-}
-
-/**
- * EXAMPLE 17: Display post meta conditionally
- */
-function example_display_post_meta() {
- if (!roi_get_option('show_post_meta', true)) {
- return;
- }
-
- ?>
- ' . __('Configure general theme settings including logo, branding, and social media.', 'roi-theme') . '
';
-}
-
-function roi_content_section_callback() {
- echo '' . __('Configure content display settings for posts, pages, and archives.', 'roi-theme') . '
';
-}
-
-function roi_performance_section_callback() {
- echo '' . __('Optimize your site performance with these settings.', 'roi-theme') . '
';
-}
-
-function roi_related_posts_section_callback() {
- echo '' . __('Configure related posts display on single post pages.', 'roi-theme') . '
';
-}
-
-function roi_social_share_section_callback() {
- echo '' . __('Configure social share buttons display on single post pages.', 'roi-theme') . '
';
-}
-
-/**
- * Sanitize all options
- *
- * @param array $input The input array
- * @return array The sanitized array
- */
-function roi_sanitize_options($input) {
- $sanitized = array();
-
- if (!is_array($input)) {
- return $sanitized;
- }
-
- // General Settings
- $sanitized['site_logo'] = isset($input['site_logo']) ? absint($input['site_logo']) : 0;
- $sanitized['site_favicon'] = isset($input['site_favicon']) ? absint($input['site_favicon']) : 0;
- $sanitized['enable_breadcrumbs'] = isset($input['enable_breadcrumbs']) ? (bool) $input['enable_breadcrumbs'] : false;
- $sanitized['breadcrumb_separator'] = isset($input['breadcrumb_separator']) ? sanitize_text_field($input['breadcrumb_separator']) : '>';
- $sanitized['date_format'] = isset($input['date_format']) ? sanitize_text_field($input['date_format']) : 'd/m/Y';
- $sanitized['time_format'] = isset($input['time_format']) ? sanitize_text_field($input['time_format']) : 'H:i';
- $sanitized['copyright_text'] = isset($input['copyright_text']) ? wp_kses_post($input['copyright_text']) : '';
-
- // Social Media
- $social_fields = array('facebook', 'twitter', 'instagram', 'linkedin', 'youtube');
- foreach ($social_fields as $social) {
- $key = 'social_' . $social;
- $sanitized[$key] = isset($input[$key]) ? esc_url_raw($input[$key]) : '';
- }
-
- // Content Settings
- $sanitized['excerpt_length'] = isset($input['excerpt_length']) ? absint($input['excerpt_length']) : 55;
- $sanitized['excerpt_more'] = isset($input['excerpt_more']) ? sanitize_text_field($input['excerpt_more']) : '...';
- $sanitized['default_post_layout'] = isset($input['default_post_layout']) ? sanitize_text_field($input['default_post_layout']) : 'right-sidebar';
- $sanitized['default_page_layout'] = isset($input['default_page_layout']) ? sanitize_text_field($input['default_page_layout']) : 'right-sidebar';
- $sanitized['archive_posts_per_page'] = isset($input['archive_posts_per_page']) ? absint($input['archive_posts_per_page']) : 10;
- $sanitized['show_featured_image_single'] = isset($input['show_featured_image_single']) ? (bool) $input['show_featured_image_single'] : false;
- $sanitized['show_author_box'] = isset($input['show_author_box']) ? (bool) $input['show_author_box'] : false;
- $sanitized['enable_comments_posts'] = isset($input['enable_comments_posts']) ? (bool) $input['enable_comments_posts'] : false;
- $sanitized['enable_comments_pages'] = isset($input['enable_comments_pages']) ? (bool) $input['enable_comments_pages'] : false;
- $sanitized['show_post_meta'] = isset($input['show_post_meta']) ? (bool) $input['show_post_meta'] : false;
- $sanitized['show_post_tags'] = isset($input['show_post_tags']) ? (bool) $input['show_post_tags'] : false;
- $sanitized['show_post_categories'] = isset($input['show_post_categories']) ? (bool) $input['show_post_categories'] : false;
-
- // Performance Settings
- $sanitized['enable_lazy_loading'] = isset($input['enable_lazy_loading']) ? (bool) $input['enable_lazy_loading'] : false;
- $sanitized['performance_remove_emoji'] = isset($input['performance_remove_emoji']) ? (bool) $input['performance_remove_emoji'] : false;
- $sanitized['performance_remove_embeds'] = isset($input['performance_remove_embeds']) ? (bool) $input['performance_remove_embeds'] : false;
- $sanitized['performance_remove_dashicons'] = isset($input['performance_remove_dashicons']) ? (bool) $input['performance_remove_dashicons'] : false;
- $sanitized['performance_defer_js'] = isset($input['performance_defer_js']) ? (bool) $input['performance_defer_js'] : false;
- $sanitized['performance_minify_html'] = isset($input['performance_minify_html']) ? (bool) $input['performance_minify_html'] : false;
- $sanitized['performance_disable_gutenberg'] = isset($input['performance_disable_gutenberg']) ? (bool) $input['performance_disable_gutenberg'] : false;
-
- // Related Posts
- $sanitized['enable_related_posts'] = isset($input['enable_related_posts']) ? (bool) $input['enable_related_posts'] : false;
- $sanitized['related_posts_count'] = isset($input['related_posts_count']) ? absint($input['related_posts_count']) : 3;
- $sanitized['related_posts_taxonomy'] = isset($input['related_posts_taxonomy']) ? sanitize_text_field($input['related_posts_taxonomy']) : 'category';
- $sanitized['related_posts_title'] = isset($input['related_posts_title']) ? sanitize_text_field($input['related_posts_title']) : __('Related Posts', 'roi-theme');
- $sanitized['related_posts_columns'] = isset($input['related_posts_columns']) ? absint($input['related_posts_columns']) : 3;
-
- // Social Share Buttons
- $sanitized['roi_enable_share_buttons'] = isset($input['roi_enable_share_buttons']) ? sanitize_text_field($input['roi_enable_share_buttons']) : '1';
- $sanitized['roi_share_text'] = isset($input['roi_share_text']) ? sanitize_text_field($input['roi_share_text']) : __('Compartir:', 'roi-theme');
-
- // Advanced Settings
- $sanitized['custom_css'] = isset($input['custom_css']) ? roi_sanitize_css($input['custom_css']) : '';
- $sanitized['custom_js_header'] = isset($input['custom_js_header']) ? roi_sanitize_js($input['custom_js_header']) : '';
- $sanitized['custom_js_footer'] = isset($input['custom_js_footer']) ? roi_sanitize_js($input['custom_js_footer']) : '';
-
- return $sanitized;
-}
-
-/**
- * NOTE: All sanitization functions have been moved to inc/sanitize-functions.php
- * to avoid function redeclaration errors. This includes:
- * - roi_sanitize_css()
- * - roi_sanitize_js()
- * - roi_sanitize_integer()
- * - roi_sanitize_text()
- * - roi_sanitize_url()
- * - roi_sanitize_html()
- * - roi_sanitize_checkbox()
- * - roi_sanitize_select()
- */
diff --git a/admin/theme-options/options-page-template.php b/admin/theme-options/options-page-template.php
deleted file mode 100644
index 7938e440..00000000
--- a/admin/theme-options/options-page-template.php
+++ /dev/null
@@ -1,659 +0,0 @@
-
-
-