Migración completa a Clean Architecture con componentes funcionales

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

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

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

View File

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