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