refactor: reorganizar openspec y planificacion con spec recaptcha

- renombrar openspec/ a _openspec/ (carpeta auxiliar)
- mover specs de features a changes/
- crear specs base: arquitectura-limpia, estandares-codigo, nomenclatura
- migrar _planificacion/ con design-system y roi-theme-template
- agregar especificacion recaptcha anti-spam (proposal, tasks, spec)
- corregir rutas y referencias en todas las specs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
FrankZamora
2026-01-08 15:30:45 -06:00
parent 0d6b6db108
commit 0f6387ab46
58 changed files with 15364 additions and 1171 deletions

View File

@@ -0,0 +1,437 @@
# WORKFLOW DE DESARROLLO ROI THEME
**ESTE ARCHIVO ES OBLIGATORIO LEER ANTES DE CUALQUIER DESARROLLO**
---
## REGLA DE ORO
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ │
│ SI NO EXISTE spec.md APROBADO → NO SE TOCA CÓDIGO │
│ │
│ NUNCA desarrollar directamente. SIEMPRE seguir el flujo de fases. │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
---
## RUTAS DEL PROYECTO
| Carpeta | Contenido | Ruta |
|---------|-----------|------|
| **Specs Base** | Arquitectura, estándares, nomenclatura | `_openspec/specs/` |
| **Specs de Cambios** | Especificaciones por funcionalidad | `_openspec/changes/` |
| **Schemas JSON** | Definición de componentes | `Schemas/` |
| **Renderers** | Componentes frontend | `Public/[Componente]/` |
| **FormBuilders** | Componentes admin | `Admin/[Componente]/` |
| **Código Compartido** | Servicios, contratos, entidades | `Shared/` |
---
## CHECKLIST OBLIGATORIO (antes de cualquier desarrollo)
```
[ ] 1. Leer este archivo (WORKFLOW-ROI-THEME.md)
[ ] 2. Leer _openspec/specs/arquitectura-limpia.md
[ ] 3. Leer _openspec/specs/estandares-codigo.md
[ ] 4. Leer _openspec/specs/nomenclatura.md
[ ] 5. Identificar qué se va a desarrollar (componente, servicio, fix)
[ ] 6. Determinar el flujo apropiado:
- Componente UI → Flujo de 5 Fases
- Servicio/Feature → Flujo de 3 Fases
- Bug fix simple → Flujo directo con spec mínima
```
---
## FLUJO DE 5 FASES (COMPONENTES UI)
> **Usar para:** Nuevos componentes visuales (Renderers + FormBuilders)
```
╔═══════════════════════════════════════════════════════════════════════════════╗
║ FASE 1: SCHEMA JSON ║
║ "Definir la estructura de datos" ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║ ║
║ Entrada: Template HTML de referencia (_planificacion/roi-theme-template/) ║
║ Salida: Schemas/[nombre-en-kebab-case].json ║
║ ║
║ Contenido obligatorio: ║
║ • component_name: kebab-case (ej: "featured-image") ║
║ • version: "1.0.0" ║
║ • description: Qué hace el componente ║
║ • groups.visibility con 3 campos: ║
║ - is_enabled (boolean, required) ║
║ - show_on_desktop (boolean) ║
║ - show_on_mobile (boolean) ║
║ • Grupos adicionales con priority 10-90 ║
║ ║
║ Agente: roi-schema-architect ║
║ ║
╚═══════════════════════════════════════════════════════════════════════════════╝
╔═══════════════════════════════════════════════════════════════════════════════╗
║ FASE 2: SINCRONIZACIÓN BD ║
║ "Registrar configuración en base de datos" ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║ ║
║ Entrada: Schema JSON creado en Fase 1 ║
║ Salida: Registros en wp_roi_theme_component_settings ║
║ ║
║ Comando: ║
║ powershell -Command "php 'C:\xampp\php_8.0.30_backup\wp-cli.phar' \ ║
║ roi-theme sync-component [nombre-en-kebab-case]" ║
║ ║
║ Verificar: ║
║ • Todos los campos del JSON existen en BD ║
║ • Valores default aplicados correctamente ║
║ • No hay campos huérfanos ║
║ ║
╚═══════════════════════════════════════════════════════════════════════════════╝
╔═══════════════════════════════════════════════════════════════════════════════╗
║ FASE 3: RENDERER (Frontend) ║
║ "Generar HTML y CSS desde BD" ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║ ║
║ Entrada: Schema + Datos en BD ║
║ Salida: Public/[PascalCase]/Infrastructure/Ui/[PascalCase]Renderer.php ║
║ ║
║ Requisitos obligatorios: ║
║ • declare(strict_types=1) ║
║ • Namespace: ROITheme\Public\[Component]\Infrastructure\Ui ║
║ • final class [Component]Renderer implements RendererInterface ║
║ • DI via constructor: CSSGeneratorInterface ║
║ • COMPONENT_NAME constante en kebab-case ║
║ • supports() retorna kebab-case ║
║ • Validar: isEnabled(), getVisibilityClass() ║
║ • CERO CSS hardcodeado (usar $this->cssGenerator) ║
║ ║
║ Agente: roi-renderer-builder ║
║ ║
╚═══════════════════════════════════════════════════════════════════════════════╝
╔═══════════════════════════════════════════════════════════════════════════════╗
║ FASE 4: FORMBUILDER (Admin) ║
║ "Panel de configuración en admin" ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║ ║
║ Entrada: Schema + Renderer funcionando ║
║ Salida: Admin/[PascalCase]/Infrastructure/Ui/[PascalCase]FormBuilder.php ║
║ ║
║ Requisitos obligatorios: ║
║ • declare(strict_types=1) ║
║ • Namespace: ROITheme\Admin\[Component]\Infrastructure\Ui ║
║ • final class [Component]FormBuilder ║
║ • DI via constructor: AdminDashboardRenderer ║
║ • Design System: gradiente #0E2337 → #1e3a5f, borde #FF8600 ║
║ • Bootstrap 5 form controls ║
║ • data-component en kebab-case ║
║ • Registrar en getComponents() con ID kebab-case ║
║ ║
║ Agente: roi-form-builder ║
║ ║
╚═══════════════════════════════════════════════════════════════════════════════╝
╔═══════════════════════════════════════════════════════════════════════════════╗
║ FASE 5: VALIDACIÓN ║
║ "Verificar que todo cumple las specs" ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║ ║
║ Entrada: Componente completo (Schema + BD + Renderer + FormBuilder) ║
║ Salida: Reporte de validación ║
║ ║
║ Comando: ║
║ php Shared/Infrastructure/Scripts/validate-architecture.php [nombre] ║
║ ║
║ Verifica: ║
║ • Estructura de carpetas correcta ║
║ • JSON válido con campos obligatorios ║
║ • Datos en BD sincronizados ║
║ • Renderer implementa RendererInterface ║
║ • FormBuilder registrado en getComponents() ║
║ • Nomenclatura correcta (PascalCase/kebab-case) ║
║ • Clean Architecture respetada (DI, capas) ║
║ ║
╚═══════════════════════════════════════════════════════════════════════════════╝
```
---
## FLUJO DE 3 FASES (SERVICIOS/FEATURES)
> **Usar para:** Nuevos servicios, features no visuales, mejoras de infraestructura
```
╔═══════════════════════════════════════════════════════════════════════════════╗
║ FASE 1: ESPECIFICACIÓN ║
║ "Definir qué se va a construir" ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║ ║
║ Crear carpeta: _openspec/changes/[nombre-feature]/ ║
║ ║
║ Archivos a crear: ║
║ • proposal.md - Por qué y qué se va a cambiar ║
║ • tasks.md - Checklist de implementación ║
║ • spec.md - Especificación detallada (formato Gherkin) ║
║ ║
║ >>> REQUIERE APROBACIÓN DEL USUARIO PARA CONTINUAR <<< ║
║ ║
╚═══════════════════════════════════════════════════════════════════════════════╝
╔═══════════════════════════════════════════════════════════════════════════════╗
║ FASE 2: IMPLEMENTACIÓN ║
║ "Construir siguiendo la spec" ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║ ║
║ Ubicación según tipo: ║
║ • Servicios compartidos → Shared/Infrastructure/Services/ ║
║ • Contratos → Shared/Domain/Contracts/ ║
║ • UseCases → [Context]/Application/UseCases/ ║
║ • Repositorios → [Context]/Infrastructure/Persistence/WordPress/ ║
║ ║
║ Requisitos: ║
║ • declare(strict_types=1) ║
║ • Namespace correcto según ubicación ║
║ • DI via constructor (interfaces, no clases concretas) ║
║ • Métodos pequeños (<30 líneas) ║
║ • Clases pequeñas (<300 líneas) ║
║ ║
╚═══════════════════════════════════════════════════════════════════════════════╝
╔═══════════════════════════════════════════════════════════════════════════════╗
║ FASE 3: INTEGRACIÓN Y PRUEBA ║
║ "Conectar y verificar" ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║ ║
║ • Integrar servicio donde se necesite ║
║ • Probar funcionalidad en navegador ║
║ • Verificar que no hay errores PHP ║
║ • Actualizar tasks.md con estado COMPLETADO ║
║ ║
╚═══════════════════════════════════════════════════════════════════════════════╝
```
---
## CÓMO CREAR NUEVA FEATURE
### Paso 1: Crear estructura de carpetas
```
_openspec/changes/[nombre-feature]/
├── proposal.md ← Por qué y qué se va a cambiar
├── tasks.md ← Checklist → evoluciona a tracking
└── spec.md ← Especificación detallada
```
### Paso 2: Crear proposal.md
```markdown
# Proposal: [Nombre Feature]
## Why
[1-2 oraciones sobre el problema a resolver]
## What Changes
- [Lista de cambios propuestos]
## Impact
- Archivos nuevos: [lista]
- Archivos modificados: [lista]
- Specs relacionadas: [lista]
```
### Paso 3: Crear tasks.md
```markdown
# Tracking: [nombre-feature]
**Estado actual:** EN PROGRESO
**Próximo paso:** [describir]
---
## Checklist
### Fase 1: Especificación
- [ ] Crear proposal.md
- [ ] Crear spec.md con Requirements y Scenarios
- [ ] Aprobar spec.md
### Fase 2: Implementación
- [ ] Crear interface (si aplica)
- [ ] Crear implementación
- [ ] Registrar en DI container (si aplica)
### Fase 3: Integración
- [ ] Integrar donde se necesite
- [ ] Probar funcionalidad
- [ ] Verificar sin errores
---
## Historial
| Fecha | Avance |
|-------|--------|
| YYYY-MM-DD | [descripción] |
```
---
## NOMENCLATURA RÁPIDA
| Contexto | Formato | Ejemplo |
|----------|---------|---------|
| component_name (JSON/BD) | kebab-case | `"featured-image"` |
| Archivo schema | kebab-case | `featured-image.json` |
| Carpeta módulo | PascalCase | `FeaturedImage/` |
| Namespace PHP | PascalCase | `ROITheme\Public\FeaturedImage\...` |
| Clase Renderer | PascalCase | `FeaturedImageRenderer` |
| Clase FormBuilder | PascalCase | `FeaturedImageFormBuilder` |
| Constante | UPPER_SNAKE | `COMPONENT_NAME` |
| Método | camelCase | `getVisibilityClass()` |
| Variable | $camelCase | `$showDesktop` |
**Conversión kebab ↔ Pascal:** `featured-image``FeaturedImage`
---
## CLEAN ARCHITECTURE RESUMIDA
```
┌─────────────────────────────────────────────────────────────────┐
│ │
│ DOMAIN │
│ (Centro, sin deps) │
│ │
│ • Entities (Component, etc.) │
│ • Contracts/Interfaces │
│ • Value Objects │
│ │
│ PROHIBIDO: WordPress, echo, print, HTML │
│ │
├─────────────────────────────────────────────────────────────────┤
│ │
│ APPLICATION │
│ (Usa Domain) │
│ │
│ • UseCases │
│ • Application Services │
│ │
│ PROHIBIDO: WordPress, acceso BD directo │
│ │
├─────────────────────────────────────────────────────────────────┤
│ │
│ INFRASTRUCTURE │
│ (Usa Domain y Application) │
│ │
│ • Ui/ (Renderers, FormBuilders) │
│ • Api/ (AJAX handlers, REST) │
│ • Persistence/ (Repositories WordPress) │
│ • Services/ (Implementaciones) │
│ │
│ PERMITIDO: WordPress, HTML, BD, APIs externas │
│ │
└─────────────────────────────────────────────────────────────────┘
Dirección de dependencias: Infrastructure → Application → Domain
(afuera depende de adentro, NUNCA al revés)
```
---
## COMANDOS WP-CLI
```powershell
# Ubicación WP-CLI
C:\xampp\php_8.0.30_backup\wp-cli.phar
# Sincronizar un componente
powershell -Command "php 'C:\xampp\php_8.0.30_backup\wp-cli.phar' roi-theme sync-component [nombre]"
# Sincronizar todos los componentes
powershell -Command "php 'C:\xampp\php_8.0.30_backup\wp-cli.phar' roi-theme sync-all-components"
# Ejemplo
powershell -Command "php 'C:\xampp\php_8.0.30_backup\wp-cli.phar' roi-theme sync-component featured-image"
```
---
## AGENTES DISPONIBLES
| Agente | Propósito | Cuándo usar |
|--------|-----------|-------------|
| roi-schema-architect | Genera JSON schemas desde HTML | Fase 1 de componentes |
| roi-renderer-builder | Genera Renderers PHP | Fase 3 de componentes |
| roi-form-builder | Genera FormBuilders PHP | Fase 4 de componentes |
---
## ESPECIFICACIONES BASE (LECTURA OBLIGATORIA)
| Spec | Ruta | Contenido |
|------|------|-----------|
| Arquitectura | `_openspec/specs/arquitectura-limpia.md` | Capas, dependencias, estructura |
| Estándares | `_openspec/specs/estandares-codigo.md` | SOLID, límites, WordPress |
| Nomenclatura | `_openspec/specs/nomenclatura.md` | Nombres, formatos, convenciones |
---
## ROLES
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ │
│ CLAUDE CODE (Yo) USUARIO (Tú) │
│ ───────────────── ──────────── │
│ │
│ • Ejecuto los agentes • Defines qué componente crear │
│ • Genero schemas JSON • Apruebas especificaciones │
│ • Genero Renderers • Apruebas diseño │
│ • Genero FormBuilders • Pruebas en navegador │
│ • Ejecuto validaciones • Verificas funcionamiento │
│ • Documento en specs • Decides prioridades │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
---
## GARANTÍAS DEL PROCESO
1. **NUNCA** se escribe código sin spec aprobada
2. **SIEMPRE** se sigue el flujo de fases
3. **CADA** componente tiene las 5 partes (Schema, BD, Renderer, FormBuilder, Validación)
4. **TODO** el CSS se genera vía CSSGenerator (cero hardcodeado)
5. **SIEMPRE** DI via constructor (interfaces, no clases concretas)
6. **NUNCA** WordPress en Domain ni Application
---
## LECCIONES APRENDIDAS
> Esta sección documenta ajustes al workflow basados en implementaciones completadas.
### Validación código vs specs (2026-01-08)
**Contexto:** Antes de mejorar specs, se validó que el código actual cumple con las especificaciones propuestas.
**Hallazgos:**
- 17 Renderers siguen el patrón correctamente
- 17 FormBuilders siguen el patrón correctamente
- 17 Schemas en kebab-case
- 43 archivos con strict_types=1
- 39 clases final
- 23 interfaces en Domain/Contracts
**Conclusión:** El código YA implementa Clean Architecture. Las mejoras a specs son documentación de patrones existentes, no nuevas invenciones.
---
**Última actualización:** 2026-01-08

View File

@@ -0,0 +1,365 @@
# Especificacion: AdSense Auto Ads Toggle
## Purpose
Define el comportamiento de un interruptor "Auto Ads" que cuando se activa, delega el control de ubicacion de anuncios a Google AdSense Auto Ads, desactivando automaticamente todas las ubicaciones manuales del tema EXCEPTO los anuncios en resultados de busqueda del plugin ROI APU Search.
**Problema que resuelve:** El usuario necesita probar Google Auto Ads sin tener que deshabilitar manualmente cada ubicacion de anuncio. Ademas, Auto Ads tiende a romper el layout de tablas HTML, por lo que se requiere excluirlas automaticamente.
**Beneficio esperado:** Comparar facilmente el rendimiento (RPM, CTR, revenue) entre ubicaciones manuales vs Auto Ads de Google, manteniendo siempre activos los anuncios en el buscador APU.
---
## Requirements
### Requirement: Campo enable_auto_ads en Schema
El schema adsense-placement.json DEBE incluir un nuevo campo para activar Auto Ads.
#### Scenario: Definicion del campo en JSON Schema
- **WHEN** se define el campo `enable_auto_ads`
- **THEN** DEBE ubicarse en el grupo `visibility` con priority 10
- **AND** DEBE tener type `boolean`
- **AND** DEBE tener default `false`
- **AND** DEBE tener label `Activar Google Auto Ads`
- **AND** DEBE tener description `Cuando esta activo, Google controla automaticamente donde mostrar anuncios. Se desactivan las ubicaciones manuales excepto los anuncios en busqueda.`
#### Scenario: Posicion del campo en el formulario
- **WHEN** se renderiza el formulario de configuracion
- **THEN** el campo `enable_auto_ads` DEBE aparecer inmediatamente despues de `is_enabled`
- **AND** DEBE tener un indicador visual destacado (badge o icono) indicando que es modo automatico
---
### Requirement: Desactivacion Automatica de Ubicaciones Manuales
Cuando Auto Ads esta activo, el sistema DEBE ignorar todas las configuraciones de ubicacion manual.
#### Scenario: Grupos afectados por enable_auto_ads
- **GIVEN** `enable_auto_ads === true`
- **WHEN** se evaluan las ubicaciones de anuncios
- **THEN** los siguientes grupos DEBEN comportarse como si estuvieran desactivados:
- `incontent_advanced` (In-Content Ads Avanzado)
- `behavior` (Ubicaciones en Posts) - EXCEPTO `javascript_first_mode`
- `anchor_ads` (Anuncios Fijos)
- `vignette_ads` (Anuncios de Vineta)
- `layout` (Ubicaciones Archivos/Globales)
- **AND** el grupo `search_results` DEBE permanecer activo
- **AND** el grupo `analytics` DEBE permanecer activo
- **AND** el grupo `content` (credenciales) DEBE permanecer activo
#### Scenario: Preservacion de configuracion en base de datos
- **GIVEN** el usuario activa `enable_auto_ads`
- **WHEN** se guardan los settings
- **THEN** los valores de los campos desactivados NO DEBEN modificarse en la BD
- **AND** cuando el usuario desactive `enable_auto_ads`, sus configuraciones previas DEBEN estar intactas
#### Scenario: Evaluacion en PHP (Server-Side)
- **GIVEN** `enable_auto_ads === true` en los settings
- **WHEN** un Renderer evalua si debe insertar un slot de anuncio
- **THEN** DEBE verificar primero si `enable_auto_ads` esta activo
- **AND** si esta activo, DEBE retornar inmediatamente sin insertar el slot manual
- **AND** EXCEPTO para slots del grupo `search_results`
#### Scenario: Evaluacion en JavaScript (Client-Side)
- **GIVEN** JavaScript-First Mode esta activo
- **AND** `enable_auto_ads === true`
- **WHEN** el controller JavaScript evalua visibilidad
- **THEN** DEBE incluir `enable_auto_ads` en el response del endpoint REST
- **AND** el script DEBE ocultar slots manuales marcados con `data-ad-manual`
- **AND** NO DEBE ocultar slots marcados con `data-ad-search`
---
### Requirement: Excepcion para ROI APU Search
Los anuncios en resultados de busqueda DEBEN permanecer activos independientemente del estado de Auto Ads.
#### Scenario: Configuracion de busqueda siempre disponible
- **GIVEN** `enable_auto_ads === true`
- **WHEN** se obtiene la configuracion para el plugin roi-apu-search via `roi_get_adsense_search_config()`
- **THEN** DEBE retornar la configuracion del grupo `search_results`
- **AND** `search_ads_enabled` DEBE evaluarse independientemente de `enable_auto_ads`
#### Scenario: Slots de busqueda con atributo especial
- **WHEN** se renderizan slots de anuncios para resultados de busqueda
- **THEN** DEBEN incluir atributo `data-ad-search="true"`
- **AND** DEBEN incluir clase CSS `roi-ad-search-slot`
- **AND** NO DEBEN incluir atributo `data-ad-manual`
#### Scenario: JavaScript no oculta slots de busqueda
- **GIVEN** el controller JavaScript detecta `enable_auto_ads === true`
- **WHEN** procesa los slots de anuncios
- **THEN** NO DEBE agregar clase `roi-ad-hidden` a elementos con `data-ad-search`
- **AND** DEBE disparar evento `roi-adsense-activate` para slots de busqueda
---
### Requirement: CSS para Excluir Tablas de Auto Ads
El sistema DEBE inyectar CSS que indica a Google Auto Ads que NO inserte anuncios dentro de tablas.
#### Scenario: Inyeccion de CSS google-auto-ads ignore
- **GIVEN** `enable_auto_ads === true`
- **WHEN** se renderiza el header del sitio
- **THEN** DEBE inyectar el siguiente CSS:
```css
table,
.wp-block-table,
.tablepress,
figure.wp-block-table,
.entry-content table {
google-auto-ads: ignore;
}
```
- **AND** el CSS DEBE cargarse ANTES del script de AdSense
#### Scenario: CSS solo cuando Auto Ads esta activo
- **GIVEN** `enable_auto_ads === false`
- **WHEN** se renderiza el header
- **THEN** NO DEBE inyectar el CSS de `google-auto-ads: ignore`
#### Scenario: Selectores adicionales configurables
- **GIVEN** el usuario necesita excluir elementos adicionales
- **WHEN** existe un campo `auto_ads_exclude_selectors` en el schema
- **THEN** DEBE agregar esos selectores al CSS de exclusion
- **AND** el campo DEBE ser de tipo `textarea`
- **AND** DEBE tener default vacio
---
### Requirement: Script de Google Auto Ads
Cuando Auto Ads esta activo, el sistema DEBE insertar el script requerido por Google.
#### Scenario: Insercion del script adsbygoogle
- **GIVEN** `enable_auto_ads === true`
- **AND** `is_enabled === true`
- **WHEN** se renderiza el head del documento
- **THEN** DEBE insertar el script de AdSense:
```html
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-XXXXXXXXXX"
crossorigin="anonymous"></script>
```
- **AND** DEBE usar el `publisher_id` configurado en el grupo `content`
#### Scenario: No duplicar scripts
- **GIVEN** el tema ya encola el script de AdSense para ubicaciones manuales
- **WHEN** `enable_auto_ads === true`
- **THEN** NO DEBE duplicar el script
- **AND** DEBE reutilizar el script existente
#### Scenario: Delay de carga respetado
- **GIVEN** `delay_enabled === true` en el grupo `forms`
- **AND** `enable_auto_ads === true`
- **WHEN** se carga la pagina
- **THEN** el script de AdSense DEBE cargarse con delay
- **AND** DEBE respetar el valor de `delay_timeout`
---
### Requirement: Indicador Visual en Admin UI
El panel de administracion DEBE mostrar claramente cuando Auto Ads esta activo.
#### Scenario: Badge de estado Auto Ads
- **GIVEN** el usuario accede al panel de configuracion de AdSense
- **WHEN** `enable_auto_ads === true`
- **THEN** DEBE mostrar un badge visible "AUTO ADS ACTIVO" en color naranja (#FF8600)
- **AND** los grupos desactivados DEBEN aparecer con opacidad reducida (0.5)
- **AND** los grupos desactivados DEBEN mostrar tooltip "Desactivado por Auto Ads"
#### Scenario: Seccion de grupos no afectados
- **GIVEN** `enable_auto_ads === true`
- **WHEN** se renderiza el formulario
- **THEN** los grupos `search_results`, `analytics`, y `content` DEBEN renderizarse normalmente
- **AND** DEBEN tener indicador "Siempre activo" visible
#### Scenario: Confirmacion al activar
- **GIVEN** el usuario hace click en `enable_auto_ads`
- **WHEN** el campo cambia de false a true
- **THEN** DEBE mostrar dialogo de confirmacion con mensaje:
"Al activar Auto Ads, Google controlara automaticamente donde mostrar anuncios.
Tus configuraciones manuales se mantendran guardadas pero no se usaran.
Los anuncios en resultados de busqueda seguiran funcionando.
Continuar?"
- **AND** DEBE tener botones "Activar Auto Ads" y "Cancelar"
---
### Requirement: Separacion de Capas segun Clean Architecture
La implementacion DEBE seguir Clean Architecture.
#### Scenario: Value Object AutoAdsConfiguration en Domain
- **WHEN** se crea el Value Object AutoAdsConfiguration
- **THEN** DEBE ubicarse en `Shared/Domain/ValueObjects/`
- **AND** DEBE ser inmutable despues de construccion
- **AND** DEBE exponer `isAutoAdsEnabled()`, `getExcludedSelectors()`, `getPublisherId()`
- **AND** NO DEBE contener logica de WordPress
#### Scenario: Interface en Domain
- **WHEN** se define el contrato para verificar si un grupo esta activo
- **THEN** DEBE existir metodo en `AdsenseSettingsInterface`:
`isGroupActiveWithAutoAds(string $groupName): bool`
- **AND** DEBE retornar false para grupos manuales cuando Auto Ads esta activo
- **AND** DEBE retornar true para `search_results`, `analytics`, `content`
#### Scenario: Service en Infrastructure
- **WHEN** se implementa la logica de evaluacion
- **THEN** el `AdsenseSettingsService` DEBE verificar `enable_auto_ads` antes de evaluar ubicaciones
- **AND** DEBE exponer metodo `getActiveGroups(): array` que retorna solo grupos activos
---
### Requirement: Endpoint REST para Estado de Auto Ads
El endpoint de visibilidad DEBE informar el estado de Auto Ads.
#### Scenario: Inclusion en response de visibility
- **GIVEN** el endpoint `/wp-json/roi-theme/v1/adsense-placement/visibility`
- **WHEN** responde exitosamente
- **THEN** DEBE incluir campo `auto_ads_enabled: boolean`
- **AND** DEBE incluir campo `auto_ads_exclude_selectors: string`
#### Scenario: Response cuando Auto Ads esta activo
- **GIVEN** `enable_auto_ads === true`
- **WHEN** se llama al endpoint
- **THEN** el response DEBE incluir:
```json
{
"show_ads": true,
"auto_ads_enabled": true,
"auto_ads_exclude_selectors": "table,.wp-block-table",
"manual_slots_disabled": true,
"search_slots_enabled": true,
"reasons": [],
"cache_seconds": 300,
"timestamp": 1733900000
}
```
---
### Requirement: Migracion y Retrocompatibilidad
La implementacion DEBE mantener compatibilidad con instalaciones existentes.
#### Scenario: Valor default para instalaciones existentes
- **GIVEN** una instalacion existente sin el campo `enable_auto_ads`
- **WHEN** se ejecuta sync-component
- **THEN** DEBE crear el campo con valor `false`
- **AND** el comportamiento existente NO DEBE cambiar
#### Scenario: Sin cambios en logica si Auto Ads esta desactivado
- **GIVEN** `enable_auto_ads === false`
- **WHEN** se evaluan ubicaciones de anuncios
- **THEN** el comportamiento DEBE ser identico al actual
- **AND** NO DEBE haber impacto en rendimiento
---
### Requirement: Logging y Diagnostico
El sistema DEBE proveer informacion de diagnostico para troubleshooting.
#### Scenario: Log cuando Auto Ads ignora ubicacion
- **GIVEN** `enable_auto_ads === true`
- **AND** WP_DEBUG === true
- **WHEN** un Renderer es llamado para ubicacion manual
- **THEN** DEBE loguear mensaje: "AdSense: Skipping manual slot '{slot_name}' - Auto Ads active"
- **AND** el log DEBE ser level DEBUG
#### Scenario: Comentario HTML de diagnostico
- **GIVEN** `enable_auto_ads === true`
- **AND** WP_DEBUG === true
- **WHEN** se renderiza el header
- **THEN** DEBE incluir comentario HTML:
```html
<!-- ROI AdSense: Auto Ads Mode Active | Manual slots: disabled | Search slots: enabled -->
```
---
## Implementation Notes
### Schema Changes (adsense-placement.json)
Agregar al grupo `visibility`:
```json
"enable_auto_ads": {
"type": "boolean",
"label": "Activar Google Auto Ads",
"default": false,
"editable": true,
"description": "Google controla automaticamente la ubicacion de anuncios. Se desactivan ubicaciones manuales excepto busqueda."
}
```
Agregar al grupo `forms` (nuevo campo):
```json
"auto_ads_exclude_selectors": {
"type": "textarea",
"label": "Selectores adicionales a excluir de Auto Ads",
"default": "",
"editable": true,
"description": "Selectores CSS adicionales donde Auto Ads no debe insertar anuncios (uno por linea)"
}
```
### CSS Default Exclusions
```css
/* Elementos que Google Auto Ads debe ignorar */
table,
.wp-block-table,
.tablepress,
figure.wp-block-table,
.entry-content table,
.roi-apu-results,
.roi-apu-result-item,
pre,
code,
.wp-block-code,
.syntax-highlighted {
google-auto-ads: ignore;
}
```
### Grupos y su comportamiento con Auto Ads
| Grupo | Con Auto Ads OFF | Con Auto Ads ON |
|-------|------------------|-----------------|
| visibility | Activo | Activo (controla is_enabled global) |
| analytics | Activo | Activo |
| content | Activo | Activo (provee publisher_id) |
| incontent_advanced | Activo | IGNORADO |
| behavior | Activo | IGNORADO (excepto javascript_first_mode) |
| anchor_ads | Activo | IGNORADO |
| vignette_ads | Activo | IGNORADO |
| search_results | Activo | ACTIVO (excepcion) |
| layout | Activo | IGNORADO |
| forms | Activo | Activo (delay, exclusiones) |
---
## Validation Checklist
Antes de considerar esta especificacion implementada:
- [ ] Campo `enable_auto_ads` existe en schema y BD
- [ ] Campo `auto_ads_exclude_selectors` existe en schema y BD
- [ ] CSS de exclusion se inyecta solo cuando Auto Ads activo
- [ ] Slots manuales no se renderizan cuando Auto Ads activo
- [ ] Slots de busqueda SI se renderizan cuando Auto Ads activo
- [ ] `roi_get_adsense_search_config()` funciona independiente de Auto Ads
- [ ] Admin UI muestra indicador visual de Auto Ads activo
- [ ] Grupos desactivados aparecen con opacidad reducida
- [ ] Endpoint REST incluye `auto_ads_enabled` en response
- [ ] JavaScript oculta slots manuales pero no de busqueda
- [ ] Script de AdSense se carga correctamente
- [ ] No hay duplicacion de scripts
- [ ] Logs de debug funcionan cuando WP_DEBUG activo
- [ ] Sync-component migra correctamente instalaciones existentes

View File

@@ -0,0 +1,309 @@
# Especificacion: Unificacion de Visibilidad AdSense para Compatibilidad con Cache
## Purpose
Unificar la logica de visibilidad de AdSense para que TODA la evaluacion dependiente de usuario
(hide_for_logged_in) se realice en el cliente via JavaScript-First, eliminando llamadas a
`is_user_logged_in()` durante el render PHP.
**Problema actual:** El `AdsensePlacementRenderer` usa `PageVisibilityHelper::shouldShow()` que
internamente llama `is_user_logged_in()` en PHP. Esto causa que el HTML generado varie segun el
estado de autenticacion, rompiendo la compatibilidad con page cache.
**Solucion:** Cuando `javascript_first_mode` esta activo, el Renderer debe generar SIEMPRE el HTML
de los slots, delegando la decision de `hide_for_logged_in` al endpoint REST y al JavaScript del cliente.
---
## Background
### Arquitectura JavaScript-First existente (spec: adsense-javascript-first)
El sistema JavaScript-First ya implementa:
1. **Endpoint REST** `/wp-json/roi-theme/v1/adsense-placement/visibility`
- Evalua `is_user_logged_in()` en tiempo real (no cacheado)
- Retorna decision `show_ads: true/false` con razones
- Headers anti-cache (no-store, no-cache)
2. **Cliente JavaScript** `adsense-visibility.js`
- Consulta endpoint via AJAX con cookies (credentials: same-origin)
- Cachea decision en localStorage
- Aplica clases CSS para mostrar/ocultar slots
### El gap actual
```
FLUJO ACTUAL (PROBLEMATICO):
Request → PHP Render → PageVisibilityHelper::shouldShow()
is_user_logged_in() ← ROMPE CACHE
HTML con/sin slots (depende de login)
Page Cache ← HTML incorrecto para otros usuarios
```
```
FLUJO DESEADO (CON ESTA SPEC):
Request → PHP Render → [javascript_first_mode=true?]
↓ SI
SIEMPRE genera HTML con slots
Page Cache ← HTML identico para todos
JS consulta endpoint REST
Muestra/oculta segun respuesta
```
---
## Requirements
### Requirement 1: Bypass de hide_for_logged_in en modo JavaScript-First
The AdsensePlacementRenderer MUST skip the `is_user_logged_in()` evaluation during PHP render
when `javascript_first_mode` is enabled.
#### Scenario: JavaScript-First activo, usuario anonimo
- **GIVEN** `javascript_first_mode` esta habilitado en settings
- **AND** `hide_for_logged_in` esta habilitado
- **AND** el visitante NO esta logueado
- **WHEN** se renderiza un slot de AdSense
- **THEN** el HTML del slot DEBE generarse (placeholders visibles)
- **AND** el JavaScript DEBE consultar el endpoint
- **AND** el endpoint retorna `show_ads: true`
- **AND** los anuncios se muestran
#### Scenario: JavaScript-First activo, usuario logueado
- **GIVEN** `javascript_first_mode` esta habilitado
- **AND** `hide_for_logged_in` esta habilitado
- **AND** el visitante ESTA logueado
- **WHEN** se renderiza un slot de AdSense
- **THEN** el HTML del slot DEBE generarse (en cache seria identico a anonimo)
- **AND** el JavaScript DEBE consultar el endpoint
- **AND** el endpoint retorna `show_ads: false` con razon `hide_for_logged_in`
- **AND** los anuncios se OCULTAN via CSS/JS
#### Scenario: JavaScript-First deshabilitado (modo legacy)
- **GIVEN** `javascript_first_mode` esta DESHABILITADO
- **AND** `hide_for_logged_in` esta habilitado
- **AND** el visitante ESTA logueado
- **WHEN** se renderiza un slot de AdSense
- **THEN** el HTML del slot NO se genera (comportamiento legacy)
- **AND** `is_user_logged_in()` SE evalua en PHP
- **BECAUSE** sin JS-First, el modo legacy es la unica opcion
---
### Requirement 2: PageVisibilityHelper debe respetar modo JavaScript-First
The PageVisibilityHelper MUST provide a method that excludes user-dependent checks when
JavaScript-First mode is active for a component.
#### Scenario: Nuevo metodo shouldShowForCache
- **GIVEN** un componente con `javascript_first_mode` habilitado
- **WHEN** se llama `PageVisibilityHelper::shouldShowForCache('adsense-placement')`
- **THEN** DEBE evaluar visibilidad por tipo de pagina (home, posts, pages, etc.)
- **AND** DEBE evaluar exclusiones por categoria, ID, URL pattern
- **AND** NO DEBE evaluar `hide_for_logged_in`
- **BECAUSE** esa evaluacion la hace el cliente
#### Scenario: Componente sin JavaScript-First usa metodo existente
- **GIVEN** un componente sin `javascript_first_mode`
- **WHEN** se llama `PageVisibilityHelper::shouldShow('otro-componente')`
- **THEN** DEBE usar flujo existente incluyendo `hide_for_logged_in`
- **BECAUSE** sin JS-First, PHP es la unica fuente de verdad
---
### Requirement 3: WordPressComponentVisibilityRepository debe soportar modo bypass
The repository MUST support skipping `is_user_logged_in()` check when requested.
#### Scenario: isNotExcluded con bypass de login check
- **GIVEN** se llama `isNotExcluded('adsense-placement', skipLoginCheck: true)`
- **WHEN** el metodo evalua exclusiones
- **THEN** NO DEBE llamar `is_user_logged_in()`
- **AND** DEBE evaluar otras exclusiones normalmente
---
### Requirement 4: El endpoint REST DEBE evaluar hide_for_logged_in
The REST endpoint `/adsense-placement/visibility` MUST evaluate `hide_for_logged_in`
and include it in the decision.
**NOTA:** Esto YA esta implementado en `AdsenseVisibilityController.php`. Solo documentamos
para claridad.
#### Scenario: Endpoint evalua usuario logueado
- **GIVEN** peticion al endpoint con cookies de sesion
- **AND** `hide_for_logged_in` esta habilitado
- **AND** el usuario ESTA logueado
- **WHEN** el endpoint procesa la peticion
- **THEN** retorna `show_ads: false`
- **AND** `reasons` incluye `hide_for_logged_in`
---
### Requirement 5: El CSS de slots ocultos NO debe afectar el layout
When JavaScript hides ad slots, they MUST collapse completely without affecting page layout.
**NOTA:** Esto YA esta implementado con `:has([data-ad-status='unfilled'])`. Documentamos para
verificar que se mantiene.
---
## Implementation
### Archivos a modificar
#### 1. AdsensePlacementRenderer.php
```php
// ANTES (linea 38-43):
public function renderSlot(array $settings, string $location): string
{
// 0. Verificar visibilidad por tipo de pagina y exclusiones
if (!PageVisibilityHelper::shouldShow('adsense-placement')) {
return '';
}
// ...
}
// DESPUES:
public function renderSlot(array $settings, string $location): string
{
// 0. Verificar visibilidad (respetando modo JS-First para cache)
$jsFirstMode = ($settings['behavior']['javascript_first_mode'] ?? false) === true;
if ($jsFirstMode) {
// En modo JS-First, usar evaluacion cache-friendly (sin is_user_logged_in)
if (!PageVisibilityHelper::shouldShowForCache('adsense-placement')) {
return '';
}
} else {
// Modo legacy: usar evaluacion completa
if (!PageVisibilityHelper::shouldShow('adsense-placement')) {
return '';
}
}
// ...
}
```
#### 2. PageVisibilityHelper.php
```php
// AGREGAR nuevo metodo:
/**
* Evalua visibilidad SIN checks dependientes de usuario
*
* Para uso cuando JavaScript manejara los checks de usuario.
* Evalua: tipos de pagina, exclusiones por categoria/ID/URL
* NO evalua: hide_for_logged_in
*
* @param string $componentName
* @return bool
*/
public static function shouldShowForCache(string $componentName): bool
{
$container = DIContainer::getInstance();
$useCase = $container->getEvaluateComponentVisibilityUseCase();
return $useCase->executeForCache($componentName);
}
```
#### 3. EvaluateComponentVisibilityUseCase.php
```php
// AGREGAR nuevo metodo:
/**
* Evalua visibilidad SIN checks dependientes de usuario
*/
public function executeForCache(string $componentName): bool
{
// Paso 1: Verificar visibilidad por tipo de pagina (sin cambios)
$visibleByPageType = $this->pageVisibilityUseCase->execute($componentName);
if (!$visibleByPageType) {
return false;
}
// Paso 2: Verificar exclusiones SIN hide_for_logged_in
$isExcluded = $this->exclusionsUseCase->executeForCache($componentName);
return !$isExcluded;
}
```
#### 4. EvaluateExclusionsUseCase.php
```php
// AGREGAR nuevo metodo:
/**
* Evalua exclusiones SIN hide_for_logged_in
*/
public function executeForCache(string $componentName): bool
{
// Evaluar exclusiones por categoria, ID, URL
// NO evaluar hide_for_logged_in
return $this->repository->isExcludedForCache($componentName);
}
```
#### 5. WordPressComponentVisibilityRepository.php
```php
// MODIFICAR isNotExcluded o agregar nuevo metodo:
/**
* Verifica exclusiones SIN evaluar hide_for_logged_in
*/
public function isNotExcludedForCache(string $componentName): bool
{
// OMITE: shouldHideForLoggedIn()
// MANTIENE: PageVisibilityHelper::shouldShow() para otras exclusiones
return PageVisibilityHelper::shouldShowByPageType($componentName);
}
```
---
## Acceptance Criteria
1. Con `javascript_first_mode=true` y `hide_for_logged_in=true`:
- Usuario anonimo: HTML generado, JS muestra ads
- Usuario logueado: HTML generado, JS oculta ads
- Page cache sirve mismo HTML a ambos
2. Con `javascript_first_mode=false`:
- Comportamiento legacy sin cambios
- `is_user_logged_in()` se evalua en PHP
3. Otras exclusiones (categoria, ID, URL) funcionan igual en ambos modos
4. No hay regresion en visibilidad por tipo de pagina
5. Los slots ocultos por JS colapsan completamente (height: 0)
---
## Migration Notes
- **NO hay breaking changes**: `shouldShow()` mantiene comportamiento actual
- **Nuevo metodo opcional**: `shouldShowForCache()` para modo JS-First
- **Backward compatible**: Si `javascript_first_mode=false`, todo funciona igual
---
## Version History
| Version | Date | Changes |
|---------|------|---------|
| 1.0 | 2025-12-11 | Initial spec |

View File

@@ -0,0 +1,214 @@
# Test Plan: Unificacion de Visibilidad AdSense para Cache
## Test Environment
- **DEV URL**: https://dev.analisisdepreciosunitarios.com
- **PROD URL**: https://analisisdepreciosunitarios.com
- **Browsers**: Chrome, Firefox
- **Tools**: DevTools Console, Network tab, Playwright MCP
---
## Pre-requisitos
1. Habilitar `javascript_first_mode` en settings de adsense-placement
2. Habilitar `hide_for_logged_in` en settings de adsense-placement
3. Tener cuenta de usuario para pruebas de login
4. Limpiar cache del sitio antes de cada prueba
---
## Test Cases
### TC01: HTML se genera para usuarios anonimos (JS-First activo)
**Objetivo**: Verificar que los slots de ads se renderizan en HTML para visitantes anonimos
| Campo | Valor |
|-------|-------|
| Pre-condicion | `javascript_first_mode=true`, `hide_for_logged_in=true`, usuario anonimo |
| Pasos | 1. Limpiar cache del sitio<br>2. Abrir pagina de post en navegacion privada<br>3. Inspeccionar HTML |
| Resultado esperado | Elementos `.roi-adsense-placeholder` presentes en DOM |
| Status | PENDING |
| Notas | |
### TC02: HTML se genera para usuarios logueados (JS-First activo)
**Objetivo**: Verificar que los slots de ads se renderizan en HTML incluso para usuarios logueados
| Campo | Valor |
|-------|-------|
| Pre-condicion | `javascript_first_mode=true`, `hide_for_logged_in=true`, usuario logueado |
| Pasos | 1. Limpiar cache del sitio<br>2. Login como usuario<br>3. Visitar pagina de post<br>4. Inspeccionar HTML |
| Resultado esperado | Elementos `.roi-adsense-placeholder` presentes en DOM (mismo HTML que TC01) |
| Status | PENDING |
| Notas | Clave: el HTML debe ser IDENTICO al de TC01 |
### TC03: Endpoint retorna show_ads=false para usuarios logueados
**Objetivo**: Verificar que el endpoint REST evalua correctamente hide_for_logged_in
| Campo | Valor |
|-------|-------|
| Pre-condicion | `javascript_first_mode=true`, `hide_for_logged_in=true`, usuario logueado |
| Pasos | 1. Login como usuario<br>2. En console ejecutar:<br>`fetch('/wp-json/roi-theme/v1/adsense-placement/visibility?post_id=1', {credentials:'same-origin'}).then(r=>r.json()).then(console.log)` |
| Resultado esperado | Respuesta: `{show_ads: false, reasons: ['hide_for_logged_in'], ...}` |
| Status | PENDING |
| Notas | |
### TC04: JavaScript oculta ads para usuarios logueados
**Objetivo**: Verificar que el JavaScript aplica correctamente la decision del endpoint
| Campo | Valor |
|-------|-------|
| Pre-condicion | `javascript_first_mode=true`, `hide_for_logged_in=true`, usuario logueado |
| Pasos | 1. Login como usuario<br>2. Visitar pagina de post<br>3. Verificar clases CSS de slots<br>4. Verificar evento `roiAdsenseDeactivated` |
| Resultado esperado | Slots tienen clase `roi-adsense-hidden` o altura colapsada |
| Status | PENDING |
| Notas | |
### TC05: JavaScript muestra ads para usuarios anonimos
**Objetivo**: Verificar que el JavaScript muestra correctamente los ads
| Campo | Valor |
|-------|-------|
| Pre-condicion | `javascript_first_mode=true`, `hide_for_logged_in=true`, usuario anonimo |
| Pasos | 1. Navegacion privada<br>2. Visitar pagina de post<br>3. Verificar clases CSS de slots |
| Resultado esperado | Slots tienen clase `roi-adsense-active` |
| Status | PENDING |
| Notas | |
### TC06: Cache sirve mismo HTML a ambos tipos de usuario
**Objetivo**: Verificar compatibilidad con page cache
| Campo | Valor |
|-------|-------|
| Pre-condicion | `javascript_first_mode=true`, page cache activo |
| Pasos | 1. Limpiar cache<br>2. Usuario anonimo visita pagina (genera cache)<br>3. Usuario logueado visita misma pagina<br>4. Comparar HTML de `.roi-adsense-placeholder` |
| Resultado esperado | HTML identico, diferencia solo en comportamiento JS |
| Status | PENDING |
| Notas | Este es el test critico de la spec |
### TC07: Modo legacy funciona sin cambios (JS-First deshabilitado)
**Objetivo**: Verificar backward compatibility
| Campo | Valor |
|-------|-------|
| Pre-condicion | `javascript_first_mode=false`, `hide_for_logged_in=true`, usuario logueado |
| Pasos | 1. Deshabilitar JS-First en settings<br>2. Login como usuario<br>3. Visitar pagina de post<br>4. Inspeccionar HTML |
| Resultado esperado | Elementos `.roi-adsense-placeholder` NO presentes (comportamiento legacy) |
| Status | PENDING |
| Notas | Comportamiento identico al actual |
### TC08: Exclusiones por tipo de pagina funcionan en ambos modos
**Objetivo**: Verificar que exclusiones no relacionadas con login siguen funcionando
| Campo | Valor |
|-------|-------|
| Pre-condicion | `show_on_archives=false` configurado |
| Pasos | 1. Visitar pagina de archivo (categoria)<br>2. Verificar que no hay slots de ads |
| Resultado esperado | Sin slots de ads en paginas de archivo |
| Status | PENDING |
| Notas | |
### TC09: Exclusiones por categoria funcionan en ambos modos
**Objetivo**: Verificar que exclusiones por categoria siguen funcionando
| Campo | Valor |
|-------|-------|
| Pre-condicion | Categoria excluida configurada |
| Pasos | 1. Visitar post de categoria excluida<br>2. Verificar que no hay slots de ads |
| Resultado esperado | Sin slots de ads en posts de categoria excluida |
| Status | PENDING |
| Notas | |
---
## Automated Test Scripts
### Script: Verificar HTML identico para cache
```javascript
// Ejecutar en Playwright o consola
// Compara DOM de slots entre usuario anonimo y logueado
async function verifyCacheCompatibility() {
const anonSlots = document.querySelectorAll('.roi-adsense-placeholder').length;
console.log(`Slots encontrados: ${anonSlots}`);
// El HTML debe existir independientemente del estado de login
// La diferencia esta en las clases CSS aplicadas por JS
const activeSlots = document.querySelectorAll('.roi-adsense-active').length;
const hiddenSlots = document.querySelectorAll('.roi-adsense-hidden').length;
console.log(`Activos: ${activeSlots}, Ocultos: ${hiddenSlots}`);
return {
totalSlots: anonSlots,
active: activeSlots,
hidden: hiddenSlots
};
}
verifyCacheCompatibility();
```
### Script: Verificar respuesta del endpoint
```javascript
// Ejecutar en consola con usuario logueado
async function testEndpoint() {
const postId = document.querySelector('article')?.id?.replace('post-', '') || '1';
const url = `/wp-json/roi-theme/v1/adsense-placement/visibility?post_id=${postId}`;
const response = await fetch(url, { credentials: 'same-origin' });
const data = await response.json();
console.log('Respuesta del endpoint:');
console.log('- show_ads:', data.show_ads);
console.log('- reasons:', data.reasons);
console.log('- cache_seconds:', data.cache_seconds);
return data;
}
testEndpoint();
```
---
## Notas de Implementacion
1. **Orden de ejecucion**: Implementar spec primero, luego ejecutar tests
2. **Rollback**: Si falla TC06, revertir cambios (backward compatible)
3. **Monitoreo**: Despues de deploy, verificar metricas de AdSense por 24h
---
## Results Summary
| Test Case | Status | Date | Tester |
|-----------|--------|------|--------|
| TC01 | PENDING | | |
| TC02 | PENDING | | |
| TC03 | PENDING | | |
| TC04 | PENDING | | |
| TC05 | PENDING | | |
| TC06 | PENDING | | |
| TC07 | PENDING | | |
| TC08 | PENDING | | |
| TC09 | PENDING | | |
---
## Version History
| Version | Date | Changes |
|---------|------|---------|
| 1.0 | 2025-12-11 | Initial test plan |

File diff suppressed because it is too large Load Diff

View File

@@ -59,7 +59,7 @@ The first step MUST be creating the component JSON schema.
#### Scenario: Fuente del schema #### Scenario: Fuente del schema
- **WHEN** se extrae informacion para el schema - **WHEN** se extrae informacion para el schema
- **THEN** DEBE basarse en _planeacion/roi-theme/roi-theme-template/index.html - **THEN** DEBE basarse en _planificacion/roi-theme-template/index.html
- **AND** DEBEN extraerse TODOS los campos CSS y textos del HTML - **AND** DEBEN extraerse TODOS los campos CSS y textos del HTML
#### Scenario: Campos obligatorios de visibilidad #### Scenario: Campos obligatorios de visibilidad

View File

@@ -0,0 +1,111 @@
# Proposal: reCAPTCHA v3 Anti-Spam Protection
## Problema
Los formularios del sitio (Newsletter Footer y Contact Form) carecen de protección CAPTCHA, haciéndolos vulnerables a spam automatizado. Actualmente solo cuentan con:
- Nonce de WordPress
- Rate limiting básico
- Sanitización de inputs
- Validación de email
## Solución Propuesta
Implementar **Google reCAPTCHA v3** como capa adicional de protección anti-spam.
### Por qué reCAPTCHA v3
| Característica | reCAPTCHA v2 | reCAPTCHA v3 |
|----------------|--------------|--------------|
| UX | Requiere interacción (checkbox/imágenes) | Invisible, sin fricción |
| Detección | Binaria (humano/bot) | Score 0.0-1.0 |
| Flexibilidad | Fija | Configurable por score |
| Impacto en conversión | Negativo | Mínimo |
### Credenciales
```
Site Key: 6LevZUQsAAAAAB6wcQ4iE6ckaTwgVR_ScBL3vqSj
```
> **Nota**: El Secret Key debe almacenarse en wp-config.php o como opción encriptada en BD, NUNCA en código fuente.
## Arquitectura Propuesta
```
Shared/
├── Domain/
│ └── Contracts/
│ └── RecaptchaValidatorInterface.php
├── Application/
│ └── Services/
│ └── RecaptchaValidationService.php
└── Infrastructure/
└── Services/
└── GoogleRecaptchaValidator.php
```
### Flujo de Validación
```
1. Frontend: Usuario envía formulario
2. Frontend: reCAPTCHA genera token automáticamente
3. Backend: AjaxHandler recibe token con datos del form
4. Backend: RecaptchaValidationService valida token con API de Google
5. Backend: Si score < threshold → rechazar como spam
6. Backend: Si score >= threshold → procesar formulario normalmente
```
## Formularios Afectados
1. **Newsletter Footer** (`Public/Footer/Infrastructure/Api/WordPress/NewsletterAjaxHandler.php`)
2. **Contact Form** (`Public/ContactForm/Infrastructure/Api/WordPress/ContactFormAjaxHandler.php`)
## Configuración Administrable
| Campo | Tipo | Default | Descripción |
|-------|------|---------|-------------|
| is_enabled | boolean | true | Habilitar/deshabilitar reCAPTCHA |
| site_key | text | - | Clave pública de reCAPTCHA |
| secret_key | text | - | Clave secreta (encriptada) |
| score_threshold | select | 0.5 | Umbral mínimo (0.3, 0.5, 0.7, 0.9) |
| action_newsletter | text | newsletter_submit | Acción para newsletter |
| action_contact | text | contact_submit | Acción para contacto |
## Impacto
### Archivos a Crear
- `Shared/Domain/Contracts/RecaptchaValidatorInterface.php`
- `Shared/Domain/Entities/RecaptchaResult.php`
- `Shared/Application/Services/RecaptchaValidationService.php`
- `Shared/Infrastructure/Services/GoogleRecaptchaValidator.php`
- `Schemas/recaptcha-settings.json`
- `Admin/RecaptchaSettings/Infrastructure/Ui/RecaptchaSettingsFormBuilder.php`
- `Admin/RecaptchaSettings/Infrastructure/FieldMapping/RecaptchaSettingsFieldMapper.php`
### Archivos a Modificar
- `Public/Footer/Infrastructure/Api/WordPress/NewsletterAjaxHandler.php`
- `Public/ContactForm/Infrastructure/Api/WordPress/ContactFormAjaxHandler.php`
- `Public/Footer/Infrastructure/Ui/FooterRenderer.php` (agregar script reCAPTCHA)
- `Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php` (agregar script reCAPTCHA)
- `functions.php` (registrar servicios en contenedor DI)
- `Admin/Infrastructure/Ui/AdminDashboardRenderer.php` (registrar tab de reCAPTCHA)
## Riesgos y Mitigaciones
| Riesgo | Probabilidad | Mitigación |
|--------|--------------|------------|
| API Google no disponible | Baja | Fallback: permitir envío (fail-open) |
| Falsos positivos | Media | Score threshold configurable |
| Latencia adicional | Baja | Validación asíncrona, timeout corto |
## Criterios de Aceptación
1. reCAPTCHA v3 integrado en ambos formularios
2. Score threshold configurable desde admin
3. Logging de intentos bloqueados
4. Sin impacto visible en UX del usuario
5. Fallback funcional si API falla
## Última actualización
2025-01-08

View File

@@ -0,0 +1,325 @@
# Especificación: reCAPTCHA v3 Anti-Spam Protection
## Purpose
Define la integración de Google reCAPTCHA v3 para proteger los formularios del sitio (Newsletter y Contact Form) contra spam automatizado, siguiendo Clean Architecture.
## Requirements
### Requirement: Configuración de reCAPTCHA
El sistema DEBE permitir configurar reCAPTCHA v3 desde el panel de administración.
#### Scenario: Schema JSON para configuración
- **WHEN** se crea el schema de configuración
- **THEN** DEBE ubicarse en `Schemas/recaptcha-settings.json`
- **AND** `component_name` DEBE ser `recaptcha-settings`
- **AND** DEBE incluir grupo VISIBILITY con `is_enabled`
- **AND** DEBE incluir grupo BEHAVIOR con `site_key`, `secret_key`, `score_threshold`
#### Scenario: Campos obligatorios del schema
- **GIVEN** el schema `recaptcha-settings.json`
- **WHEN** se define la estructura
- **THEN** `is_enabled` DEBE ser tipo boolean con default true
- **AND** `site_key` DEBE ser tipo text (clave pública)
- **AND** `secret_key` DEBE ser tipo text (clave secreta, almacenada encriptada)
- **AND** `score_threshold` DEBE ser tipo select con options: 0.3, 0.5, 0.7, 0.9
#### Scenario: Sincronización con BD
- **WHEN** se sincroniza el schema
- **THEN** ejecutar `wp roi-theme sync-component recaptcha-settings`
- **AND** los datos DEBEN ir a `wp_roi_theme_component_settings`
- **AND** `component_name` en BD DEBE ser `recaptcha-settings`
---
### Requirement: Contrato de Validación (Domain)
El Domain DEBE definir la interfaz de validación de reCAPTCHA.
#### Scenario: Ubicación de RecaptchaValidatorInterface
- **WHEN** se crea la interfaz
- **THEN** DEBE ubicarse en `Shared/Domain/Contracts/RecaptchaValidatorInterface.php`
- **AND** namespace DEBE ser `ROITheme\Shared\Domain\Contracts`
#### Scenario: Firma del método validate
- **WHEN** se define RecaptchaValidatorInterface
- **THEN** DEBE tener método `validate(string $token, string $action): RecaptchaResult`
- **AND** `$token` es el token generado por reCAPTCHA frontend
- **AND** `$action` es el nombre de la acción (newsletter_submit, contact_submit)
- **AND** retorna objeto `RecaptchaResult` con score y success
#### Scenario: Entidad RecaptchaResult
- **WHEN** se define el resultado de validación
- **THEN** DEBE existir `Shared/Domain/Entities/RecaptchaResult.php`
- **AND** DEBE tener propiedades: `success` (bool), `score` (float), `action` (string), `errorCodes` (array)
- **AND** DEBE tener método `isValid(float $threshold): bool`
---
### Requirement: Servicio de Aplicación
La capa Application DEBE orquestar la validación de reCAPTCHA.
#### Scenario: Ubicación del servicio
- **WHEN** se crea el servicio de aplicación
- **THEN** DEBE ubicarse en `Shared/Application/Services/RecaptchaValidationService.php`
- **AND** namespace DEBE ser `ROITheme\Shared\Application\Services`
#### Scenario: Inyección de dependencias
- **WHEN** se implementa RecaptchaValidationService
- **THEN** DEBE inyectar `RecaptchaValidatorInterface` via constructor
- **AND** DEBE inyectar `ComponentSettingsRepositoryInterface` para obtener configuración
- **AND** NO DEBE instanciar servicios directamente con `new`
#### Scenario: Lógica de validación con threshold
- **GIVEN** un token de reCAPTCHA y una acción
- **WHEN** se llama a `validateSubmission(string $token, string $action): bool`
- **THEN** DEBE obtener `score_threshold` de la configuración en BD
- **AND** DEBE llamar a `RecaptchaValidatorInterface::validate()`
- **AND** DEBE retornar `true` si `RecaptchaResult::isValid($threshold)` es true
- **AND** DEBE retornar `false` si score está por debajo del threshold
#### Scenario: Bypass cuando está deshabilitado
- **GIVEN** `is_enabled` es false en configuración
- **WHEN** se llama a `validateSubmission()`
- **THEN** DEBE retornar `true` sin llamar a la API de Google
- **AND** NO DEBE generar errores
---
### Requirement: Implementación de Infraestructura
La capa Infrastructure DEBE implementar la comunicación con API de Google.
#### Scenario: Ubicación del validador
- **WHEN** se implementa el validador
- **THEN** DEBE ubicarse en `Shared/Infrastructure/Services/GoogleRecaptchaValidator.php`
- **AND** namespace DEBE ser `ROITheme\Shared\Infrastructure\Services`
- **AND** DEBE implementar `RecaptchaValidatorInterface`
#### Scenario: Llamada a API de Google
- **WHEN** se valida un token
- **THEN** DEBE hacer POST a `https://www.google.com/recaptcha/api/siteverify`
- **AND** DEBE enviar `secret` (secret key) y `response` (token)
- **AND** DEBE usar `wp_remote_post()` de WordPress
- **AND** timeout DEBE ser máximo 5 segundos
#### Scenario: Parseo de respuesta exitosa
- **GIVEN** API de Google responde exitosamente
- **WHEN** se parsea la respuesta
- **THEN** DEBE extraer `success` (bool)
- **AND** DEBE extraer `score` (float 0.0-1.0)
- **AND** DEBE extraer `action` (string)
- **AND** DEBE retornar `RecaptchaResult` con estos valores
#### Scenario: Manejo de errores de API
- **GIVEN** API de Google no responde o responde con error
- **WHEN** se procesa la respuesta
- **THEN** DEBE retornar `RecaptchaResult` con `success = false`
- **AND** DEBE incluir códigos de error en `errorCodes`
- **AND** NO DEBE lanzar excepciones no controladas
---
### Requirement: Integración Frontend
Los Renderers DEBEN incluir el script de reCAPTCHA y generar tokens.
#### Scenario: Script de reCAPTCHA en FooterRenderer
- **WHEN** FooterRenderer genera HTML del newsletter
- **AND** reCAPTCHA está habilitado
- **THEN** DEBE incluir script: `<script src="https://www.google.com/recaptcha/api.js?render={site_key}"></script>`
- **AND** DEBE agregar input hidden `recaptcha_token` al formulario
- **AND** DEBE agregar JS para ejecutar `grecaptcha.execute()` al submit
#### Scenario: Script de reCAPTCHA en ContactFormRenderer
- **WHEN** ContactFormRenderer genera HTML del formulario
- **AND** reCAPTCHA está habilitado
- **THEN** DEBE incluir script de reCAPTCHA con site_key
- **AND** DEBE agregar input hidden `recaptcha_token`
- **AND** DEBE agregar JS para ejecutar `grecaptcha.execute()` al submit
#### Scenario: JavaScript de ejecución de reCAPTCHA
- **WHEN** usuario hace submit del formulario
- **THEN** JS DEBE interceptar el submit
- **AND** DEBE llamar `grecaptcha.execute(siteKey, {action: 'action_name'})`
- **AND** DEBE esperar el token (Promise)
- **AND** DEBE insertar token en input hidden
- **AND** DEBE continuar con el submit del formulario
#### Scenario: No cargar script si está deshabilitado
- **GIVEN** reCAPTCHA `is_enabled` es false
- **WHEN** se renderiza el formulario
- **THEN** NO DEBE incluir script de reCAPTCHA
- **AND** NO DEBE agregar input hidden de token
- **AND** formulario DEBE funcionar normalmente
---
### Requirement: Integración Backend (AjaxHandlers)
Los AjaxHandlers DEBEN validar el token de reCAPTCHA antes de procesar.
#### Scenario: Validación en NewsletterAjaxHandler
- **WHEN** se procesa suscripción de newsletter
- **AND** reCAPTCHA está habilitado
- **THEN** DEBE obtener `recaptcha_token` del POST
- **AND** DEBE llamar a `RecaptchaValidationService::validateSubmission()`
- **AND** si retorna false, DEBE responder con error JSON
- **AND** si retorna true, DEBE continuar procesamiento normal
#### Scenario: Validación en ContactFormAjaxHandler
- **WHEN** se procesa envío de formulario de contacto
- **AND** reCAPTCHA está habilitado
- **THEN** DEBE obtener `recaptcha_token` del POST
- **AND** DEBE llamar a `RecaptchaValidationService::validateSubmission()`
- **AND** si retorna false, DEBE responder con error JSON
- **AND** si retorna true, DEBE continuar procesamiento normal
#### Scenario: Mensaje de error por reCAPTCHA fallido
- **GIVEN** validación de reCAPTCHA falla
- **WHEN** AjaxHandler responde
- **THEN** DEBE retornar JSON con `success: false`
- **AND** mensaje DEBE ser genérico: "No se pudo verificar que eres humano. Intenta de nuevo."
- **AND** NO DEBE revelar detalles técnicos del score
#### Scenario: Inyección de dependencias en AjaxHandlers
- **WHEN** se modifican los AjaxHandlers
- **THEN** DEBEN inyectar `RecaptchaValidationService` via constructor
- **AND** NO DEBE instanciar servicios directamente
- **AND** DEBE seguir principio de Inversión de Dependencias
---
### Requirement: Panel de Administración
DEBE existir un FormBuilder para configurar reCAPTCHA.
#### Scenario: Ubicación del FormBuilder
- **WHEN** se crea el FormBuilder
- **THEN** DEBE ubicarse en `Admin/RecaptchaSettings/Infrastructure/Ui/RecaptchaSettingsFormBuilder.php`
- **AND** namespace DEBE ser `ROITheme\Admin\RecaptchaSettings\Infrastructure\Ui`
#### Scenario: Registro en getComponents
- **WHEN** se registra el FormBuilder
- **THEN** DEBE registrarse en `getComponents()` con ID `recaptcha-settings`
- **AND** DEBE aparecer en el menú del admin dashboard
#### Scenario: Campos del formulario admin
- **WHEN** se renderiza el formulario de configuración
- **THEN** DEBE mostrar toggle para `is_enabled`
- **AND** DEBE mostrar input text para `site_key`
- **AND** DEBE mostrar input password para `secret_key`
- **AND** DEBE mostrar select para `score_threshold` con opciones 0.3, 0.5, 0.7, 0.9
- **AND** DEBE seguir Design System: gradiente #0E2337#1e3a5f, borde #FF8600
#### Scenario: FieldMapper para mapeo de campos
- **WHEN** se crea el componente admin
- **THEN** DEBE existir `Admin/RecaptchaSettings/Infrastructure/FieldMapping/RecaptchaSettingsFieldMapper.php`
- **AND** DEBE implementar `FieldMapperInterface`
- **AND** DEBE registrarse en `FieldMapperRegistry`
---
### Requirement: Seguridad
La implementación DEBE seguir mejores prácticas de seguridad.
#### Scenario: Almacenamiento de secret key
- **WHEN** se guarda el secret key
- **THEN** DEBE almacenarse encriptado en BD
- **AND** NO DEBE aparecer en código fuente
- **AND** NO DEBE exponerse en frontend
#### Scenario: Site key en frontend
- **GIVEN** site key es público por diseño de Google
- **WHEN** se incluye en frontend
- **THEN** PUEDE incluirse en atributo de script
- **AND** DEBE obtenerse de configuración en BD
#### Scenario: Validación de token en backend
- **WHEN** se recibe token de reCAPTCHA
- **THEN** DEBE sanitizarse con `sanitize_text_field()`
- **AND** DEBE validarse que no esté vacío
- **AND** DEBE enviarse a API de Google para verificación real
#### Scenario: No confiar solo en frontend
- **GIVEN** tokens pueden ser fabricados
- **WHEN** se valida reCAPTCHA
- **THEN** SIEMPRE DEBE verificarse con API de Google en backend
- **AND** NUNCA confiar en validación solo de frontend
---
### Requirement: Logging y Monitoreo
El sistema DEBE registrar intentos de spam bloqueados.
#### Scenario: Log de intentos bloqueados
- **WHEN** reCAPTCHA bloquea un intento
- **THEN** DEBE registrar en log de WordPress
- **AND** DEBE incluir: timestamp, IP, action, score obtenido, threshold configurado
- **AND** NO DEBE incluir datos personales del usuario
#### Scenario: Log de errores de API
- **WHEN** API de Google falla
- **THEN** DEBE registrar error en log
- **AND** DEBE incluir código de error y mensaje
- **AND** DEBE permitir diagnóstico del problema
---
### Requirement: Fallback y Resiliencia
El sistema DEBE manejar fallos graciosamente.
#### Scenario: Fail-open cuando API no responde
- **GIVEN** API de Google no responde (timeout)
- **WHEN** se intenta validar
- **THEN** DEBE permitir el envío del formulario (fail-open)
- **AND** DEBE registrar el evento en log
- **AND** NO DEBE bloquear usuarios legítimos por falla de terceros
#### Scenario: Degradación cuando reCAPTCHA deshabilitado
- **GIVEN** administrador deshabilita reCAPTCHA
- **WHEN** se envía formulario
- **THEN** DEBE procesarse normalmente
- **AND** validaciones existentes (nonce, rate limit) DEBEN seguir activas
- **AND** NO DEBE generar errores por ausencia de token
---
## Checklist de Implementación
### Archivos a Crear
- [ ] `Schemas/recaptcha-settings.json`
- [ ] `Shared/Domain/Contracts/RecaptchaValidatorInterface.php`
- [ ] `Shared/Domain/Entities/RecaptchaResult.php`
- [ ] `Shared/Application/Services/RecaptchaValidationService.php`
- [ ] `Shared/Infrastructure/Services/GoogleRecaptchaValidator.php`
- [ ] `Admin/RecaptchaSettings/Infrastructure/Ui/RecaptchaSettingsFormBuilder.php`
- [ ] `Admin/RecaptchaSettings/Infrastructure/FieldMapping/RecaptchaSettingsFieldMapper.php`
### Archivos a Modificar
- [ ] `Public/Footer/Infrastructure/Ui/FooterRenderer.php`
- [ ] `Public/Footer/Infrastructure/Api/WordPress/NewsletterAjaxHandler.php`
- [ ] `Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php`
- [ ] `Public/ContactForm/Infrastructure/Api/WordPress/ContactFormAjaxHandler.php`
- [ ] `functions.php` (registro DI)
- [ ] `Admin/Infrastructure/Ui/AdminDashboardRenderer.php` (registrar componente)
- [ ] `Admin/Shared/Infrastructure/FieldMapping/FieldMapperRegistry.php`
### Validaciones
- [ ] Schema tiene campos de visibilidad
- [ ] Domain no tiene dependencias de Infrastructure
- [ ] Application solo depende de interfaces de Domain
- [ ] Todos los servicios inyectados via constructor
- [ ] CSS generado via CSSGeneratorService (si aplica)
- [ ] Secret key nunca expuesto en frontend
---
## Última actualización
2025-01-08

View File

@@ -0,0 +1,132 @@
# Tasks: reCAPTCHA v3 Anti-Spam Protection
## Fase 1: Especificación
- [x] Crear proposal.md
- [x] Crear tasks.md
- [x] Crear spec.md con formato Gherkin
- [ ] Obtener aprobación del usuario
## Fase 2: Implementación
### 2.1 Capa Domain (Contratos y Entidades)
- [ ] Crear `Shared/Domain/Contracts/RecaptchaValidatorInterface.php`
```php
interface RecaptchaValidatorInterface {
public function validate(string $token, string $action): RecaptchaResult;
}
```
- [ ] Crear `Shared/Domain/Entities/RecaptchaResult.php`
```php
final class RecaptchaResult {
public function __construct(
private bool $success,
private float $score,
private string $action,
private array $errorCodes = []
) {}
public function isValid(float $threshold): bool;
}
```
### 2.2 Capa Application (Servicios)
- [ ] Crear `Shared/Application/Services/RecaptchaValidationService.php`
- Orquestar validación
- Aplicar threshold configurable
- Logging de resultados
### 2.3 Capa Infrastructure (Implementación)
- [ ] Crear `Shared/Infrastructure/Services/GoogleRecaptchaValidator.php`
- Llamada HTTP a API de Google
- Manejo de errores y timeout
- Parseo de respuesta JSON
### 2.4 Schema y Admin UI
- [ ] Crear `Schemas/recaptcha-settings.json`
- Campos: is_enabled, site_key, secret_key, score_threshold, actions
- [ ] Sincronizar schema con BD: `wp roi-theme sync-component recaptcha-settings`
- [ ] Crear `Admin/RecaptchaSettings/Infrastructure/Ui/RecaptchaSettingsFormBuilder.php`
- [ ] Crear `Admin/RecaptchaSettings/Infrastructure/FieldMapping/RecaptchaSettingsFieldMapper.php`
- [ ] Registrar en `getComponents()` del AdminDashboardRenderer
- [ ] Registrar FieldMapper en FieldMapperRegistry
### 2.5 Integración Frontend
- [ ] Modificar `FooterRenderer.php`
- Agregar script de reCAPTCHA con site key
- Modificar form para incluir token hidden
- [ ] Modificar `ContactFormRenderer.php`
- Agregar script de reCAPTCHA con site key
- Modificar form para incluir token hidden
- [ ] Crear JS compartido para ejecutar reCAPTCHA y obtener token
### 2.6 Integración Backend
- [ ] Modificar `NewsletterAjaxHandler.php`
- Inyectar RecaptchaValidationService
- Validar token antes de procesar
- Retornar error si score bajo
- [ ] Modificar `ContactFormAjaxHandler.php`
- Inyectar RecaptchaValidationService
- Validar token antes de procesar
- Retornar error si score bajo
### 2.7 Registro DI
- [ ] Modificar `functions.php`
- Registrar RecaptchaValidatorInterface → GoogleRecaptchaValidator
- Registrar RecaptchaValidationService
## Fase 3: Integración y Validación
### 3.1 Testing Manual
- [ ] Probar Newsletter con reCAPTCHA habilitado
- [ ] Probar Contact Form con reCAPTCHA habilitado
- [ ] Probar con reCAPTCHA deshabilitado (fallback)
- [ ] Probar cambio de threshold desde admin
- [ ] Verificar logging de intentos
### 3.2 Validación de Arquitectura
- [ ] Ejecutar `validate-architecture.php recaptcha-settings`
- [ ] Verificar cumplimiento Clean Architecture
- [ ] Verificar inyección de dependencias correcta
### 3.3 Documentación
- [ ] Actualizar CLAUDE.md si es necesario
- [ ] Documentar configuración en admin
## Dependencias
| Tarea | Depende de |
|-------|------------|
| Application Service | Domain Contract |
| Infrastructure Service | Domain Contract |
| Admin FormBuilder | Schema JSON sincronizado |
| Frontend integration | Site Key configurado |
| Backend integration | Application Service + Infrastructure |
## Estimación de Archivos
| Tipo | Cantidad |
|------|----------|
| Nuevos | 7 |
| Modificados | 7 |
| Total | 14 |
### Archivos Nuevos
1. `Shared/Domain/Contracts/RecaptchaValidatorInterface.php`
2. `Shared/Domain/Entities/RecaptchaResult.php`
3. `Shared/Application/Services/RecaptchaValidationService.php`
4. `Shared/Infrastructure/Services/GoogleRecaptchaValidator.php`
5. `Schemas/recaptcha-settings.json`
6. `Admin/RecaptchaSettings/Infrastructure/Ui/RecaptchaSettingsFormBuilder.php`
7. `Admin/RecaptchaSettings/Infrastructure/FieldMapping/RecaptchaSettingsFieldMapper.php`
### Archivos a Modificar
1. `Public/Footer/Infrastructure/Api/WordPress/NewsletterAjaxHandler.php`
2. `Public/ContactForm/Infrastructure/Api/WordPress/ContactFormAjaxHandler.php`
3. `Public/Footer/Infrastructure/Ui/FooterRenderer.php`
4. `Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php`
5. `functions.php`
6. `Admin/Infrastructure/Ui/AdminDashboardRenderer.php`
7. `Admin/Shared/Infrastructure/FieldMapping/FieldMapperRegistry.php`
## Última actualización
2025-01-08

View File

@@ -66,6 +66,9 @@ El sistema maneja componentes UI configurables para un sitio de análisis de pre
- **FormBuilder**: Panel admin para configurar valores - **FormBuilder**: Panel admin para configurar valores
### Flujo de 5 Fases para Componentes ### Flujo de 5 Fases para Componentes
> **Nota**: `[nombre]` = kebab-case (ej: `contact-form`), `[Nombre]` = PascalCase (ej: `ContactForm`)
1. Schema JSON → `Schemas/[nombre].json` 1. Schema JSON → `Schemas/[nombre].json`
2. Sincronización → `wp roi-theme sync-component [nombre]` 2. Sincronización → `wp roi-theme sync-component [nombre]`
3. Renderer → `Public/[Nombre]/Infrastructure/Ui/[Nombre]Renderer.php` 3. Renderer → `Public/[Nombre]/Infrastructure/Ui/[Nombre]Renderer.php`
@@ -86,6 +89,20 @@ El sistema maneja componentes UI configurables para un sitio de análisis de pre
- WP-CLI (`C:\xampp\php_8.0.30_backup\wp-cli.phar`) - WP-CLI (`C:\xampp\php_8.0.30_backup\wp-cli.phar`)
## Referencias Documentación ## Referencias Documentación
- Arquitectura: `_planeacion/roi-theme/_arquitectura/`
- Template HTML: `_planeacion/roi-theme/roi-theme-template/index.html` ### OpenSpec - Especificaciones del Proyecto
- Design System: `_planeacion/roi-theme/_arquitectura/01-design-system/` | Archivo | Descripción | Ubicación |
|---------|-------------|-----------|
| **WORKFLOW-ROI-THEME.md** | Flujo de trabajo obligatorio, regla de oro | `_openspec/WORKFLOW-ROI-THEME.md` |
| **AGENTS.md** | Agentes disponibles y cuándo usarlos | `_openspec/AGENTS.md` |
| **arquitectura-limpia** | Clean Architecture, capas, estructura | `_openspec/specs/arquitectura-limpia.md` |
| **estandares-codigo** | SOLID, PHP, WordPress, seguridad | `_openspec/specs/estandares-codigo.md` |
| **nomenclatura** | Convenciones de nombres completas | `_openspec/specs/nomenclatura.md` |
### Documentación de Planificación
- Template HTML: `_planificacion/roi-theme-template/index.html`
- Design System: `_planificacion/01-design-system/`
---
**Última actualización:** 2026-01-08

View File

@@ -0,0 +1,777 @@
# Especificacion de Arquitectura Limpia
## Purpose
Define la implementacion de Clean Architecture para ROITheme, un tema WordPress que sigue principios de Domain-Driven Design con separacion fisica de contextos delimitados (Admin, Public, Shared).
> **NOTA**: Para convenciones de nomenclatura, ver `_openspec/specs/nomenclatura.md`
> **NOTA**: Para principios SOLID y estandares de codigo, ver `_openspec/specs/estandares-codigo.md`
> **NOTA**: Para flujo de trabajo y fases obligatorias, ver `_openspec/WORKFLOW-ROI-THEME.md`
---
## Requirements
### Requirement: Separacion Fisica de Contextos
The system MUST organize code into three physically delimited contexts: Admin/, Public/, and Shared/.
#### Scenario: Codigo pertenece al contexto de administracion
- **WHEN** el codigo maneja operaciones CRUD, configuracion o funcionalidad del panel admin
- **THEN** el codigo DEBE colocarse en el directorio `Admin/`
- **AND** el codigo NO DEBE importar del directorio `Public/`
#### Scenario: Codigo pertenece al contexto publico/frontend
- **WHEN** el codigo maneja renderizado, visualizacion o presentacion frontend
- **THEN** el codigo DEBE colocarse en el directorio `Public/`
- **AND** el codigo NO DEBE importar del directorio `Admin/`
#### Scenario: Codigo es compartido entre contextos
- **WHEN** el codigo es usado por AMBOS contextos Admin/ y Public/
- **THEN** el codigo DEBE colocarse en el directorio `Shared/` raiz
- **AND** tanto Admin/ como Public/ PUEDEN importar de Shared/
---
### Requirement: Organizacion Granular de Codigo Compartido
The system MUST implement three levels of shared code to avoid mixing context-specific shared code.
#### Scenario: Codigo compartido solo dentro del contexto Admin
- **WHEN** el codigo es reutilizado por multiples modulos Admin pero NO por Public
- **THEN** el codigo DEBE colocarse en el directorio `Admin/Shared/`
- **AND** los modulos de Public/ NO DEBEN importar de `Admin/Shared/`
#### Scenario: Codigo compartido solo dentro del contexto Public
- **WHEN** el codigo es reutilizado por multiples modulos Public pero NO por Admin
- **THEN** el codigo DEBE colocarse en el directorio `Public/Shared/`
- **AND** los modulos de Admin/ NO DEBEN importar de `Public/Shared/`
#### Scenario: Codigo compartido entre ambos contextos
- **WHEN** el codigo es reutilizado por AMBOS modulos Admin/ y Public/
- **THEN** el codigo DEBE colocarse en el directorio `Shared/` raiz
- **AND** esto incluye ValueObjects, Exceptions y Contracts base
---
### Requirement: Cada Contexto Sigue las Capas de Clean Architecture
Each context (Admin/, Public/, Shared/) MUST implement Infrastructure layer, and MAY implement Domain and Application layers when business logic requires it.
#### Scenario: Estructura de modulo dentro del contexto Admin
- **GIVEN** un componente llamado "Navbar" en el contexto Admin
- **WHEN** el modulo es creado
- **THEN** la estructura DEBE incluir: Admin/Navbar/Infrastructure/
- **AND** PUEDE incluir Domain/ (si hay logica de negocio)
- **AND** PUEDE incluir Application/ (si hay casos de uso)
#### Scenario: Estructura de modulo dentro del contexto Public
- **GIVEN** un componente llamado "Navbar" en el contexto Public
- **WHEN** el modulo es creado
- **THEN** la estructura DEBE incluir: Public/Navbar/Infrastructure/
- **AND** PUEDE incluir Domain/ (si hay logica de negocio)
- **AND** PUEDE incluir Application/ (si hay casos de uso)
---
### Requirement: Cumplimiento de Direccion de Dependencias
The system MUST enforce that dependencies flow ONLY from outer layers to inner layers.
#### Scenario: Infrastructure depende de Application y Domain
- **WHEN** el codigo esta en la capa Infrastructure
- **THEN** PUEDE importar de la capa Application
- **AND** PUEDE importar de la capa Domain
#### Scenario: Application depende solo de Domain
- **WHEN** el codigo esta en la capa Application
- **THEN** PUEDE importar de la capa Domain
- **AND** NO DEBE importar de la capa Infrastructure
#### Scenario: Domain no tiene dependencias externas
- **WHEN** el codigo esta en la capa Domain
- **THEN** NO DEBE importar de la capa Application
- **AND** NO DEBE importar de la capa Infrastructure
- **AND** NO DEBE importar funciones o globales de WordPress
---
### Requirement: La Capa Domain Contiene Solo Logica de Negocio Pura
The Domain layer MUST contain only pure business logic without framework dependencies.
#### Scenario: Validacion de contenido de capa Domain
- **WHEN** el codigo se coloca en la capa Domain
- **THEN** PUEDE contener Entities, Value Objects, Domain Services, Interfaces, Exceptions
- **AND** NO DEBE contener global $wpdb, $_POST, $_GET, $_SESSION, add_action, add_filter, HTML, CSS, JavaScript
#### Scenario: Implementacion de entidad Domain
- **GIVEN** una entidad Domain como NavbarConfiguration
- **WHEN** la entidad es implementada
- **THEN** DEBE contener reglas de negocio y validacion
- **AND** NO DEBE contener logica de persistencia
- **AND** NO DEBE referenciar APIs de WordPress
---
### Requirement: La Capa Application Orquesta Domain
The Application layer MUST orchestrate domain entities without containing business logic.
#### Scenario: Implementacion de Use Case
- **WHEN** un Use Case es implementado
- **THEN** DEBE coordinar entidades y servicios de domain
- **AND** DEBE depender de interfaces, NO de implementaciones concretas
- **AND** NO DEBE contener reglas de validacion de negocio
#### Scenario: Uso de DTOs para transferencia de datos
- **WHEN** los datos cruzan limites entre capas
- **THEN** se DEBEN usar DTOs (Data Transfer Objects)
- **AND** los DTOs DEBEN ser contenedores de datos simples sin logica de negocio
---
### Requirement: Infrastructure Implementa Interfaces
The Infrastructure layer MUST implement interfaces defined in Domain/Application layers.
#### Scenario: Implementacion de Repository
- **GIVEN** una RepositoryInterface definida en Domain
- **WHEN** el repository es implementado
- **THEN** DEBE colocarse en Infrastructure/Persistence/
- **AND** DEBE implementar la interface de Domain
- **AND** PUEDE usar global $wpdb o APIs de WordPress
#### Scenario: Integracion con WordPress
- **WHEN** se necesita codigo especifico de WordPress
- **THEN** DEBE colocarse en la capa Infrastructure
- **AND** NO DEBE filtrarse a las capas Domain o Application
---
### Requirement: Los Modulos Son Autocontenidos e Independientes
Each module (Navbar, Footer, Toolbar, etc.) MUST be self-contained and independent from other modules.
#### Scenario: Aislamiento de modulos
- **WHEN** un modulo como Admin/Navbar/ es implementado
- **THEN** NO DEBE importar de Admin/Footer/
- **AND** NO DEBE importar de Admin/Toolbar/
- **AND** SOLO PUEDE importar de Shared/
#### Scenario: Eliminacion de modulos
- **WHEN** un modulo necesita ser eliminado
- **THEN** borrar la carpeta del modulo NO DEBE romper otros modulos
- **AND** no se DEBERIAN requerir cambios de codigo en otros modulos
---
### Requirement: Admin y Public Son Bounded Contexts Separados
Admin/ and Public/ MUST be treated as separate bounded contexts because they have different responsibilities.
#### Scenario: Responsabilidad del contexto Admin
- **WHEN** el codigo maneja administracion de componentes
- **THEN** la entidad Domain se enfoca en configuracion, validacion, estados draft/published
- **AND** los Use Cases se enfocan en operaciones Save, Update, Delete, Get
#### Scenario: Responsabilidad del contexto Public
- **WHEN** el codigo maneja renderizado de componentes
- **THEN** la entidad Domain se enfoca en estado activo, caching, filtrado por permisos
- **AND** los Use Cases se enfocan en operaciones GetActive, Render, Cache
#### Scenario: No hay duplicacion de domain
- **WHEN** Admin/Navbar/Domain/ y Public/Navbar/Domain/ ambos existen
- **THEN** NO son duplicados sino bounded contexts especializados
- **AND** Admin se enfoca en configuracion/gestion
- **AND** Public se enfoca en renderizado/visualizacion
---
### Requirement: Validacion de Arquitectura Antes de Commit
The system MUST validate architectural compliance before committing code.
#### Scenario: Validacion de capa Domain
- **WHEN** se valida codigo de la capa Domain
- **THEN** grep por global $wpdb DEBE retornar vacio
- **AND** grep por add_action DEBE retornar vacio
- **AND** grep por $_POST DEBE retornar vacio
#### Scenario: Validacion de dependencias de modulos
- **WHEN** se validan dependencias entre modulos
- **THEN** imports de Admin/Navbar/ desde Admin/Footer/ NO DEBEN existir
- **AND** imports de Public/Navbar/ desde Public/Footer/ NO DEBEN existir
---
### Requirement: Realizacion de Beneficios de la Arquitectura
The architecture MUST provide measurable benefits.
#### Scenario: Asignacion granular de trabajo
- **WHEN** un desarrollador es asignado a trabajar en Admin/Navbar/
- **THEN** puede acceder SOLO a esa carpeta
- **AND** no puede ver ni modificar Public/ u otros modulos de Admin/
#### Scenario: Eliminacion facil de modulos
- **WHEN** un componente ya no es necesario
- **THEN** eliminarlo requiere solo borrar la carpeta
- **AND** no se necesitan otras modificaciones de codigo
#### Scenario: Codigo compartido consistente
- **WHEN** se encuentra un bug en un ValueObject compartido
- **THEN** arreglarlo en Shared/Domain/ValueObjects/ lo arregla para TODOS los modulos
- **AND** no se necesita actualizar codigo duplicado
---
## Diagrama ASCII de Capas
```
╔═══════════════════════════════════════════════════════════════════════════════╗
║ CLEAN ARCHITECTURE ║
║ ROI Theme ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║ ║
║ ┌─────────────────────────────────────────────────────────────────────┐ ║
║ │ │ ║
║ │ INFRASTRUCTURE │ ║
║ │ (Capa Externa - WordPress) │ ║
║ │ │ ║
║ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ │ ║
║ │ │ Ui/ │ │ Api/ │ │ Persistence/│ │ Services/ │ │ ║
║ │ │ Renderers │ │ AJAX │ │ Repositories│ │ Helpers │ │ ║
║ │ │ FormBuilders│ │ Handlers │ │ $wpdb │ │ │ │ ║
║ │ └─────────────┘ └─────────────┘ └─────────────┘ └───────────┘ │ ║
║ │ │ ║
║ │ PERMITIDO: WordPress APIs, HTML, CSS, JS, $wpdb, hooks │ ║
║ │ │ ║
║ └─────────────────────────────────────────────────────────────────────┘ ║
║ │ ║
║ ▼ depende de ║
║ ┌─────────────────────────────────────────────────────────────────────┐ ║
║ │ │ ║
║ │ APPLICATION │ ║
║ │ (Casos de Uso / Orquestacion) │ ║
║ │ │ ║
║ │ ┌─────────────────────────────────────────────────────────────┐ │ ║
║ │ │ UseCases/ │ │ ║
║ │ │ GetComponentSettingsUseCase, RenderComponentUseCase │ │ ║
║ │ │ CheckVisibilityUseCase, SyncSchemaUseCase │ │ ║
║ │ └─────────────────────────────────────────────────────────────┘ │ ║
║ │ │ ║
║ │ PERMITIDO: Orquestacion, DTOs, llamadas a interfaces Domain │ ║
║ │ PROHIBIDO: WordPress, HTML, $wpdb, persistencia directa │ ║
║ │ │ ║
║ └─────────────────────────────────────────────────────────────────────┘ ║
║ │ ║
║ ▼ depende de ║
║ ┌─────────────────────────────────────────────────────────────────────┐ ║
║ │ │ ║
║ │ DOMAIN │ ║
║ │ (Centro - Logica de Negocio Pura) │ ║
║ │ │ ║
║ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────────────┐ │ ║
║ │ │ Entities/ │ │Contracts/ │ │ Value │ │ Exceptions/ │ │ ║
║ │ │Component │ │Interfaces │ │ Objects/ │ │ValidationException│ │ ║
║ │ └───────────┘ └───────────┘ └───────────┘ └───────────────────┘ │ ║
║ │ │ ║
║ │ PERMITIDO: Reglas de negocio puras, validaciones, interfaces │ ║
║ │ PROHIBIDO: WordPress, HTML, CSS, JS, $wpdb, echo, print │ ║
║ │ │ ║
║ └─────────────────────────────────────────────────────────────────────┘ ║
║ ║
║ REGLA DE DEPENDENCIA: Las flechas SOLO apuntan hacia adentro ║
║ Infrastructure → Application → Domain ║
║ NUNCA al reves: Domain NO depende de nada externo ║
║ ║
╚═══════════════════════════════════════════════════════════════════════════════╝
```
---
## Estructura Completa de Carpetas del Tema
```
roi-theme/
├── functions.php # Bootstrap del tema
├── style.css # Metadata del tema
├── Schemas/ # JSON schemas de componentes
│ ├── contact-form.json
│ ├── featured-image.json
│ ├── footer.json
│ └── ...
├── Admin/ # CONTEXTO: Panel de administracion
│ ├── ContactForm/ # Modulo: Configuracion de ContactForm
│ │ ├── Domain/ # (opcional si no hay logica especifica)
│ │ ├── Application/ # (opcional si no hay use cases)
│ │ └── Infrastructure/
│ │ └── Ui/
│ │ └── ContactFormFormBuilder.php
│ │
│ ├── FeaturedImage/
│ │ └── Infrastructure/
│ │ └── Ui/
│ │ └── FeaturedImageFormBuilder.php
│ │
│ ├── Shared/ # Compartido SOLO dentro de Admin
│ │ └── Infrastructure/
│ │ └── Ui/
│ │ ├── AdminDashboardRenderer.php
│ │ └── ExclusionFormPartial.php
│ │
│ └── ... # Otros modulos Admin
├── Public/ # CONTEXTO: Frontend publico
│ ├── ContactForm/ # Modulo: Renderizado de ContactForm
│ │ ├── Domain/ # (opcional)
│ │ │ └── Contracts/
│ │ ├── Application/ # (opcional)
│ │ │ └── UseCases/
│ │ └── Infrastructure/
│ │ ├── Ui/
│ │ │ └── ContactFormRenderer.php
│ │ └── Api/
│ │ └── WordPress/
│ │ └── ContactFormAjaxHandler.php
│ │
│ ├── FeaturedImage/
│ │ └── Infrastructure/
│ │ └── Ui/
│ │ └── FeaturedImageRenderer.php
│ │
│ ├── Shared/ # Compartido SOLO dentro de Public
│ │ └── Infrastructure/
│ │ └── Services/
│ │
│ └── ... # Otros modulos Public
├── Shared/ # CONTEXTO: Compartido entre Admin Y Public
│ ├── Domain/
│ │ ├── Contracts/ # Interfaces compartidas
│ │ │ ├── RendererInterface.php
│ │ │ ├── CSSGeneratorInterface.php
│ │ │ ├── ComponentRepositoryInterface.php
│ │ │ └── ...
│ │ ├── Entities/
│ │ │ └── Component.php
│ │ └── ValueObjects/
│ │
│ ├── Application/
│ │ └── UseCases/
│ │ └── CheckWrapperVisibilityUseCase.php
│ │
│ └── Infrastructure/
│ ├── Services/
│ │ ├── CSSGeneratorService.php
│ │ └── PageVisibilityHelper.php
│ ├── Persistence/
│ │ └── WordPress/
│ │ ├── ComponentSettingsRepository.php
│ │ └── PageVisibilityRepository.php
│ └── Scripts/
│ └── validate-architecture.php
├── _openspec/ # Sistema de especificaciones
│ ├── AGENTS.md
│ ├── WORKFLOW-ROI-THEME.md
│ ├── project.md
│ ├── specs/ # Specs BASE (archivos planos)
│ │ ├── arquitectura-limpia.md
│ │ ├── estandares-codigo.md
│ │ └── nomenclatura.md
│ └── changes/ # Specs de features (carpetas)
│ └── [nombre-feature]/
└── _planificacion/ # Documentos de planificacion
├── 01-design-system/ # Design System del tema
├── roi-theme-template/ # Template HTML de referencia
├── analisis-spam-formularios.md
└── plan-mejora-especificaciones-openspec.md
```
---
## Reglas de Anidamiento
### Requirement: Profundidad Maxima de Carpetas
La estructura de carpetas DEBE respetar una profundidad maxima.
#### Scenario: Profundidad maxima de 4 niveles desde contexto
- **WHEN** se crea una estructura de carpetas
- **THEN** la profundidad maxima DEBE ser 4 niveles desde el contexto
- **AND** ejemplo valido: `Public/ContactForm/Infrastructure/Ui/` (4 niveles)
- **AND** ejemplo valido: `Public/ContactForm/Infrastructure/Api/WordPress/` (5 niveles - excepcion para WordPress)
- **AND** ejemplo invalido: `Public/ContactForm/Infrastructure/Ui/Partials/Helpers/` (6 niveles)
#### Scenario: Regla de 3 archivos para subcarpetas
- **WHEN** se decide crear una subcarpeta
- **THEN** DEBE haber al menos 3 archivos que la justifiquen
- **AND** si hay menos de 3 archivos, mantenerlos en la carpeta padre
- **AND** ejemplo: NO crear `Ui/Helpers/` con solo 1-2 archivos
#### Scenario: Subcarpetas permitidas en Infrastructure
- **WHEN** se organizan archivos dentro de Infrastructure/
- **THEN** subcarpetas permitidas son:
- `Ui/` - Renderers, FormBuilders, presentacion
- `Api/` - Handlers AJAX, REST endpoints
- `Api/WordPress/` - Handlers especificos de WordPress
- `Persistence/` - Repositorios genericos
- `Persistence/WordPress/` - Repositorios WordPress
- `Services/` - Servicios de infraestructura
- **AND** NO crear subcarpetas adicionales sin justificacion
---
## Diferencia Entre Niveles de Shared
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ NIVELES DE CODIGO COMPARTIDO │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ NIVEL 1: Shared/ (raiz) │
│ ───────────────────── │
│ QUIEN PUEDE USAR: Admin/ y Public/ │
│ CONTENIDO: Contratos base, entidades core, servicios fundamentales │
│ EJEMPLOS: │
│ - Shared/Domain/Contracts/RendererInterface.php │
│ - Shared/Domain/Contracts/CSSGeneratorInterface.php │
│ - Shared/Domain/Entities/Component.php │
│ - Shared/Infrastructure/Services/CSSGeneratorService.php │
│ │
│ NIVEL 2: Admin/Shared/ │
│ ────────────────────── │
│ QUIEN PUEDE USAR: SOLO modulos dentro de Admin/ │
│ CONTENIDO: UI components admin, helpers de formularios, partials │
│ EJEMPLOS: │
│ - Admin/Shared/Infrastructure/Ui/AdminDashboardRenderer.php │
│ - Admin/Shared/Infrastructure/Ui/ExclusionFormPartial.php │
│ PROHIBIDO PARA: Public/ │
│ │
│ NIVEL 3: Public/Shared/ │
│ ─────────────────────── │
│ QUIEN PUEDE USAR: SOLO modulos dentro de Public/ │
│ CONTENIDO: Helpers de renderizado, componentes frontend compartidos │
│ EJEMPLOS: │
│ - Public/Shared/Infrastructure/Services/RenderHelper.php │
│ PROHIBIDO PARA: Admin/ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
REGLA: Siempre colocar codigo en el nivel MAS ESPECIFICO posible.
Solo subir a Shared/ raiz si AMBOS contextos lo necesitan.
```
---
## Ejemplos de Codigo PHP Por Capa
### Ejemplo CORRECTO: Capa Domain (Interface)
```php
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Domain\Contracts;
use ROITheme\Shared\Domain\Entities\Component;
/**
* Interface para renderizadores de componentes.
*
* NOTA: Esta interface esta en Domain porque define el CONTRATO
* que deben cumplir los renderizadores, sin detalles de implementacion.
*/
interface RendererInterface
{
/**
* Renderiza un componente y retorna HTML.
*/
public function render(Component $component): string;
/**
* Verifica si este renderer soporta el tipo de componente.
*/
public function supports(string $componentType): bool;
}
```
### Ejemplo INCORRECTO: Capa Domain (Interface con WordPress)
```php
<?php
// ❌ INCORRECTO: WordPress en Domain
namespace ROITheme\Shared\Domain\Contracts;
interface RendererInterface
{
// ❌ INCORRECTO: Dependencia de WordPress (WP_Post)
public function render(WP_Post $post): string;
// ❌ INCORRECTO: HTML en Domain
public function getDefaultHtml(): string;
}
```
### Ejemplo CORRECTO: Capa Domain (Entity)
```php
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Domain\Entities;
/**
* Entidad que representa un componente del tema.
* Contiene SOLO logica de negocio pura.
*/
final class Component
{
public function __construct(
private string $name,
private array $data
) {}
public function getName(): string
{
return $this->name;
}
public function getData(): array
{
return $this->data;
}
public function isEnabled(): bool
{
return ($this->data['visibility']['is_enabled'] ?? false) === true;
}
}
```
### Ejemplo INCORRECTO: Capa Domain (Entity con WordPress)
```php
<?php
// ❌ INCORRECTO: WordPress y persistencia en Domain
namespace ROITheme\Shared\Domain\Entities;
final class Component
{
// ❌ INCORRECTO: Acceso a BD en Domain
public function save(): void
{
global $wpdb; // ❌ PROHIBIDO
$wpdb->insert('wp_components', $this->data);
}
// ❌ INCORRECTO: HTML en Domain
public function renderHtml(): string
{
return '<div>' . esc_html($this->name) . '</div>'; // ❌ PROHIBIDO
}
}
```
### Ejemplo CORRECTO: Capa Application (UseCase)
```php
<?php
declare(strict_types=1);
namespace ROITheme\Public\ContactForm\Application\UseCases;
use ROITheme\Shared\Domain\Contracts\ComponentRepositoryInterface;
use ROITheme\Shared\Domain\Entities\Component;
/**
* Caso de uso: Obtener configuracion de un componente.
* Orquesta llamadas a repositorios, NO contiene logica de negocio.
*/
final class GetComponentSettingsUseCase
{
public function __construct(
private ComponentRepositoryInterface $repository // ✅ Interface, no clase concreta
) {}
public function execute(string $componentName): ?Component
{
// ✅ Solo orquestacion, delega a repository
return $this->repository->findByName($componentName);
}
}
```
### Ejemplo INCORRECTO: Capa Application (UseCase con WordPress)
```php
<?php
// ❌ INCORRECTO: WordPress directo en Application
namespace ROITheme\Public\ContactForm\Application\UseCases;
final class GetComponentSettingsUseCase
{
public function execute(string $componentName): array
{
// ❌ INCORRECTO: $wpdb directo en Application
global $wpdb;
return $wpdb->get_row("SELECT * FROM ...");
// ❌ INCORRECTO: Funciones WordPress en Application
return get_option('roi_component_' . $componentName);
}
}
```
### Ejemplo CORRECTO: Capa Infrastructure (Renderer)
```php
<?php
declare(strict_types=1);
namespace ROITheme\Public\ContactForm\Infrastructure\Ui;
use ROITheme\Shared\Domain\Contracts\RendererInterface;
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
use ROITheme\Shared\Domain\Entities\Component;
/**
* Renderer para el formulario de contacto.
* Implementa RendererInterface de Domain.
*/
final class ContactFormRenderer implements RendererInterface
{
private const COMPONENT_NAME = 'contact-form';
public function __construct(
private CSSGeneratorInterface $cssGenerator // ✅ DI via interface
) {}
public function render(Component $component): string
{
$data = $component->getData();
// ✅ WordPress permitido en Infrastructure
$nonce = wp_create_nonce('roi_contact_form');
// ✅ HTML permitido en Infrastructure
$html = '<form id="roiContactForm" data-nonce="' . esc_attr($nonce) . '">';
// ... mas HTML
$html .= '</form>';
return $html;
}
public function supports(string $componentType): bool
{
return $componentType === self::COMPONENT_NAME;
}
}
```
### Ejemplo CORRECTO: Capa Infrastructure (Repository)
```php
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Infrastructure\Persistence\WordPress;
use ROITheme\Shared\Domain\Contracts\ComponentRepositoryInterface;
use ROITheme\Shared\Domain\Entities\Component;
/**
* Repository WordPress para componentes.
* Implementa interface de Domain usando $wpdb.
*/
final class ComponentSettingsRepository implements ComponentRepositoryInterface
{
public function findByName(string $componentName): ?Component
{
global $wpdb; // ✅ WordPress permitido en Infrastructure
$table = $wpdb->prefix . 'roi_theme_component_settings';
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$table} WHERE component_name = %s",
$componentName
),
ARRAY_A
);
if (empty($results)) {
return null;
}
return new Component($componentName, $this->groupResults($results));
}
}
```
---
## Mapeo de Terminologia
| Clean Architecture Estandar | ROI Theme | Ubicacion |
|---------------------------|-----------|-----------|
| Entity | Component, Entity | `*/Domain/Entities/` |
| Value Object | Value Object | `*/Domain/ValueObjects/` |
| Repository Interface | RepositoryInterface | `Shared/Domain/Contracts/` |
| Repository Implementation | Repository | `*/Infrastructure/Persistence/WordPress/` |
| Use Case / Interactor | UseCase | `*/Application/UseCases/` |
| Gateway | Repository | `*/Infrastructure/Persistence/` |
| Presenter | Renderer | `Public/*/Infrastructure/Ui/` |
| Controller | FormBuilder, Handler | `Admin/*/Infrastructure/Ui/`, `*/Infrastructure/Api/` |
| DTO | Request/Response | `*/Application/DTOs/` |
| Domain Service | Service | `*/Domain/Services/` |
| Infrastructure Service | Service | `*/Infrastructure/Services/` |
---
## Validacion de Arquitectura
### Script de Validacion
Ubicacion: `Shared/Infrastructure/Scripts/validate-architecture.php`
```bash
# Validar un componente especifico
php Shared/Infrastructure/Scripts/validate-architecture.php contact-form
# Validar todos los componentes
php Shared/Infrastructure/Scripts/validate-architecture.php --all
```
### Que Valida el Script
| Validacion | Que Busca | Error si Encuentra |
|------------|-----------|-------------------|
| WordPress en Domain | `global $wpdb`, `add_action`, `$_POST` | "WordPress code in Domain layer" |
| HTML en Domain | `<div`, `<form`, `echo` | "HTML/Output in Domain layer" |
| Imports cruzados | `Admin/X` importando de `Admin/Y` | "Cross-module import detected" |
| Direccion dependencias | Application importando Infrastructure | "Invalid dependency direction" |
| Nomenclatura | Carpetas no-PascalCase | "Invalid folder naming" |
| Interface implementation | Renderer sin RendererInterface | "Missing interface implementation" |
| Strict types | Archivo sin `declare(strict_types=1)` | "Missing strict_types declaration" |
### Checklist Pre-Commit de Arquitectura
```
[ ] Archivos Domain NO contienen: $wpdb, add_action, $_POST, echo, HTML
[ ] Archivos Application NO contienen: $wpdb, HTML, WordPress functions
[ ] Clases Infrastructure implementan interfaces de Domain
[ ] No hay imports cruzados entre modulos del mismo contexto
[ ] Carpetas siguen nomenclatura PascalCase
[ ] Archivos PHP tienen declare(strict_types=1)
[ ] DI es via constructor con interfaces
```
---
**Última actualización:** 2026-01-08

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,687 @@
# Especificacion de Nomenclatura - ROI Theme
## Purpose
Define las convenciones de nomenclatura para carpetas, archivos, clases, metodos, propiedades y variables en el tema ROI Theme (PHP 8.x / WordPress).
> **NOTA**: Para principios SOLID y estandares de codigo, ver `_openspec/specs/estandares-codigo.md`
> **NOTA**: Para arquitectura y modularidad, ver `_openspec/specs/arquitectura-limpia.md`
---
## Resumen de Nomenclaturas
| Elemento | Nomenclatura | Ejemplo |
|----------|-------------|---------|
| **Carpetas principales** | PascalCase | `Admin/`, `Public/`, `Shared/` |
| **Carpetas de contexto** | PascalCase | `Admin/`, `Public/` |
| **Carpetas de modulo** | PascalCase | `ContactForm/`, `FeaturedImage/` |
| **Carpetas de capa** | PascalCase | `Domain/`, `Application/`, `Infrastructure/` |
| **Carpetas auxiliares** | _minusculas | `_planificacion/`, `_arquitectura/` |
| **Archivos PHP de clase** | PascalCase.php | `ContactFormRenderer.php` |
| **Archivos PHP de interface** | PascalCaseInterface.php | `RendererInterface.php` |
| **Archivos JSON schema** | kebab-case.json | `contact-form.json` |
| **Namespaces** | PascalCase | `ROITheme\Public\ContactForm\Infrastructure\Ui` |
| **Clases** | PascalCase | `ContactFormRenderer` |
| **Interfaces** | PascalCase + Interface | `RendererInterface`, `CSSGeneratorInterface` |
| **Metodos** | camelCase | `render()`, `getVisibilityClass()` |
| **Propiedades** | camelCase | `$cssGenerator`, `$componentName` |
| **Variables locales** | $camelCase | `$showDesktop`, `$visibilityClass` |
| **Parametros** | $camelCase | `$component`, `$data` |
| **Campos privados** | $camelCase | `$cssGenerator` (con private) |
| **Constantes clase** | UPPER_SNAKE_CASE | `COMPONENT_NAME`, `MAX_ITEMS` |
| **Constantes globales** | UPPER_SNAKE_CASE | `ROI_THEME_VERSION` |
| **component_name** | kebab-case | `"contact-form"`, `"featured-image"` |
| **Hooks WordPress** | snake_case con prefijo | `roi_theme_after_render` |
---
## Requirements
### Requirement: Nomenclatura de Carpetas
Los nombres de carpetas siguen convenciones basadas en su proposito.
#### Scenario: Carpetas principales del tema
- **WHEN** se nombra una carpeta principal de codigo
- **THEN** DEBE usar PascalCase
- **AND** ejemplos correctos: `Admin/`, `Public/`, `Shared/`, `Schemas/`
- **AND** ejemplos incorrectos: `admin/`, `ADMIN/`, `shared_code/`
#### Scenario: Carpetas de contexto
- **WHEN** se nombra una carpeta que representa un contexto de la aplicacion
- **THEN** DEBE usar PascalCase
- **AND** carpetas permitidas:
- `Admin/` - Componentes del panel de administracion
- `Public/` - Componentes del frontend publico
- `Shared/` - Codigo compartido entre contextos
- **AND** ejemplos incorrectos: `backend/`, `frontend/`, `common/`
#### Scenario: Carpetas de modulo (componente)
- **WHEN** se nombra una carpeta que representa un modulo/componente
- **THEN** DEBE seguir PascalCase
- **AND** DEBE coincidir con component_name convertido de kebab-case
- **AND** ejemplos correctos:
- `ContactForm/` (de `contact-form`)
- `FeaturedImage/` (de `featured-image`)
- `TopNotificationBar/` (de `top-notification-bar`)
- `CtaBoxSidebar/` (de `cta-box-sidebar`)
- **AND** ejemplos incorrectos:
- `contact-form/` (kebab-case)
- `contactForm/` (camelCase)
- `CONTACT_FORM/` (UPPER_SNAKE)
#### Scenario: Carpetas de capa (Clean Architecture)
- **WHEN** se nombra una carpeta que representa una capa de arquitectura
- **THEN** DEBE usar PascalCase
- **AND** carpetas permitidas dentro de cada modulo:
- `Domain/` - Entidades, interfaces, value objects
- `Application/` - Casos de uso
- `Infrastructure/` - Implementaciones (Ui, Api, Persistence, Services)
- **AND** subcarpetas de Infrastructure permitidas:
- `Ui/` - Renderers, FormBuilders
- `Api/` - Handlers AJAX, REST endpoints
- `Persistence/` - Repositorios
- `Services/` - Servicios de infraestructura
- `WordPress/` - Integraciones especificas de WordPress
- **AND** ejemplos correctos: `Infrastructure/Ui/`, `Infrastructure/Api/WordPress/`
- **AND** ejemplos incorrectos: `infrastructure/`, `UI/`, `api/`
#### Scenario: Carpetas auxiliares (no codigo)
- **WHEN** se nombra una carpeta de documentacion, planificacion o configuracion
- **THEN** DEBE usar prefijo guion bajo + minusculas
- **AND** ejemplos correctos:
- `_planificacion/` - Documentos de planificacion
- `_arquitectura/` - Documentos de arquitectura
- **AND** nombres compuestos usan guion medio: `_pruebas-regresion/`
- **AND** el guion bajo indica carpetas auxiliares/no-codigo
#### Scenario: Carpetas de cambios (OpenSpec)
- **WHEN** se crea una carpeta para un cambio/feature en `_openspec/changes/`
- **THEN** DEBE usar nombre en kebab-case descriptivo
- **AND** maximo 1 nivel de carpeta despues de `changes/`
- **AND** ejemplos correctos:
- `anti-spam-validator/`
- `lazy-loading-images/`
- `improved-caching/`
- **AND** ejemplos incorrectos:
- `AntiSpam/` (PascalCase)
- `anti_spam/` (snake_case)
- `anti-spam/subfolder/` (anidamiento prohibido)
#### Scenario: Excepciones permitidas
- **WHEN** existen carpetas especiales del sistema
- **THEN** se permiten las siguientes excepciones:
- `.git/` - Control de versiones
- `.serena/` - Configuracion de Serena MCP
- `.claude/` - Configuracion de Claude Code
- `node_modules/` - Dependencias npm (si aplica)
- `vendor/` - Dependencias Composer (si aplica)
- `_openspec/` - Sistema de especificaciones (carpeta auxiliar)
---
### Requirement: Nomenclatura de Archivos PHP
Los archivos PHP DEBEN seguir convencion PascalCase.
#### Scenario: Archivos de clase
- **WHEN** se nombra un archivo que contiene una clase PHP
- **THEN** DEBE seguir PascalCase
- **AND** el nombre DEBE coincidir EXACTAMENTE con el nombre de la clase
- **AND** extension `.php`
- **AND** ejemplos correctos:
- `ContactFormRenderer.php` (contiene `class ContactFormRenderer`)
- `NewsletterAjaxHandler.php` (contiene `class NewsletterAjaxHandler`)
- `CSSGeneratorService.php` (contiene `class CSSGeneratorService`)
- **AND** ejemplos incorrectos:
- `contact-form-renderer.php` (kebab-case)
- `contactFormRenderer.php` (camelCase)
- `class_contact_form.php` (snake_case con prefijo)
#### Scenario: Archivos de interface
- **WHEN** se nombra un archivo que contiene una interface
- **THEN** DEBE seguir PascalCase con sufijo Interface
- **AND** extension `.php`
- **AND** ejemplos correctos:
- `RendererInterface.php`
- `CSSGeneratorInterface.php`
- `ComponentRepositoryInterface.php`
- **AND** ejemplos incorrectos:
- `IRenderer.php` (prefijo I estilo C#)
- `Renderer.php` (sin sufijo)
#### Scenario: Archivos de trait
- **WHEN** se nombra un archivo que contiene un trait PHP
- **THEN** DEBE seguir PascalCase con sufijo Trait
- **AND** extension `.php`
- **AND** ejemplos correctos:
- `VisibilityTrait.php`
- `CSSGeneratorTrait.php`
---
### Requirement: Nomenclatura de Archivos JSON (Schemas)
Los schemas JSON DEBEN usar kebab-case.
#### Scenario: Archivos de schema de componente
- **WHEN** se nombra un archivo JSON schema
- **THEN** DEBE usar kebab-case
- **AND** extension `.json`
- **AND** el nombre DEBE coincidir con el component_name
- **AND** ejemplos correctos:
- `contact-form.json` (component_name: "contact-form")
- `featured-image.json` (component_name: "featured-image")
- `top-notification-bar.json` (component_name: "top-notification-bar")
- `cta-box-sidebar.json` (component_name: "cta-box-sidebar")
- **AND** ejemplos incorrectos:
- `ContactForm.json` (PascalCase)
- `contact_form.json` (snake_case)
- `contactForm.json` (camelCase)
---
### Requirement: Nomenclatura de Namespaces
Los namespaces PHP DEBEN seguir convencion PascalCase jerarquica.
#### Scenario: Namespace raiz
- **WHEN** se define el namespace principal del tema
- **THEN** DEBE ser `ROITheme`
- **AND** ejemplo: `namespace ROITheme;`
#### Scenario: Namespaces de modulo publico
- **WHEN** se define un namespace para un componente en Public/
- **THEN** DEBE seguir `ROITheme\Public\[Componente]\[Capa][\Subcapa]`
- **AND** ejemplos correctos:
```php
// Renderers
namespace ROITheme\Public\ContactForm\Infrastructure\Ui;
namespace ROITheme\Public\FeaturedImage\Infrastructure\Ui;
namespace ROITheme\Public\TopNotificationBar\Infrastructure\Ui;
// AJAX Handlers
namespace ROITheme\Public\Footer\Infrastructure\Api\WordPress;
namespace ROITheme\Public\ContactForm\Infrastructure\Api\WordPress;
// Domain (si existe)
namespace ROITheme\Public\AdsensePlacement\Domain\ValueObjects;
namespace ROITheme\Public\AdsensePlacement\Domain\Contracts;
// Application (si existe)
namespace ROITheme\Public\CustomCSSManager\Application\UseCases;
```
#### Scenario: Namespaces de modulo admin
- **WHEN** se define un namespace para un componente en Admin/
- **THEN** DEBE seguir `ROITheme\Admin\[Componente]\[Capa][\Subcapa]`
- **AND** ejemplos correctos:
```php
// FormBuilders
namespace ROITheme\Admin\ContactForm\Infrastructure\Ui;
namespace ROITheme\Admin\FeaturedImage\Infrastructure\Ui;
// Shared de Admin
namespace ROITheme\Admin\Shared\Infrastructure\Ui;
```
#### Scenario: Namespaces de Shared
- **WHEN** se define un namespace para codigo compartido
- **THEN** DEBE seguir `ROITheme\Shared\[Capa][\Subcapa]`
- **AND** ejemplos correctos:
```php
// Domain
namespace ROITheme\Shared\Domain\Contracts;
namespace ROITheme\Shared\Domain\Entities;
namespace ROITheme\Shared\Domain\ValueObjects;
// Application
namespace ROITheme\Shared\Application\UseCases;
// Infrastructure
namespace ROITheme\Shared\Infrastructure\Services;
namespace ROITheme\Shared\Infrastructure\Persistence\WordPress;
```
---
### Requirement: Nomenclatura de Clases
Los nombres de clase DEBEN seguir convencion PascalCase.
#### Scenario: Clases regulares
- **WHEN** se define una clase en PHP
- **THEN** el nombre DEBE seguir PascalCase
- **AND** DEBE ser un sustantivo o frase sustantiva
- **AND** DEBE ser descriptivo de su responsabilidad
- **AND** ejemplos correctos:
```php
final class ContactFormRenderer
final class ComponentSettings
final class VisibilityChecker
```
- **AND** ejemplos incorrectos:
```php
final class contactFormRenderer // camelCase
final class contact_form_renderer // snake_case
final class Render // verbo, no sustantivo
```
#### Scenario: Clases Renderer
- **WHEN** se define una clase que renderiza HTML de un componente
- **THEN** DEBE usar sufijo `Renderer`
- **AND** DEBE implementar `RendererInterface`
- **AND** DEBE ser `final`
- **AND** ejemplos correctos:
```php
final class ContactFormRenderer implements RendererInterface
final class FeaturedImageRenderer implements RendererInterface
final class TopNotificationBarRenderer implements RendererInterface
```
#### Scenario: Clases FormBuilder
- **WHEN** se define una clase que genera formularios admin
- **THEN** DEBE usar sufijo `FormBuilder`
- **AND** DEBE ser `final`
- **AND** ejemplos correctos:
```php
final class ContactFormFormBuilder
final class FeaturedImageFormBuilder
final class TopNotificationBarFormBuilder
```
#### Scenario: Clases de caso de uso
- **WHEN** se define una clase UseCase en Application/
- **THEN** DEBE usar sufijo `UseCase`
- **AND** el nombre DEBE describir la accion
- **AND** ejemplos correctos:
```php
final class GetCriticalCSSUseCase
final class CheckAdsenseVisibilityUseCase
final class GetDeferredSnippetsUseCase
```
#### Scenario: Clases de servicio
- **WHEN** se define una clase que provee servicios en Infrastructure/
- **THEN** DEBE usar sufijo `Service`
- **AND** ejemplos correctos:
```php
final class CSSGeneratorService
final class AntiSpamValidatorService
final class CacheService
```
#### Scenario: Clases de repositorio
- **WHEN** se define una clase de acceso a datos
- **THEN** DEBE usar sufijo `Repository`
- **AND** ejemplos correctos:
```php
final class ComponentSettingsRepository
final class PageVisibilityRepository
```
#### Scenario: Clases Handler (AJAX/API)
- **WHEN** se define una clase que maneja peticiones AJAX o API
- **THEN** DEBE usar sufijo `Handler` o `Controller`
- **AND** ejemplos correctos:
```php
final class NewsletterAjaxHandler
final class ContactFormAjaxHandler
final class AdsenseVisibilityController
```
---
### Requirement: Nomenclatura de Interfaces
Los nombres de interface DEBEN seguir convencion PascalCase con sufijo.
#### Scenario: Interfaces
- **WHEN** se define una interface en PHP
- **THEN** el nombre DEBE terminar con `Interface`
- **AND** DEBE seguir PascalCase
- **AND** DEBE describir la capacidad o contrato
- **AND** ejemplos correctos:
```php
interface RendererInterface
interface CSSGeneratorInterface
interface ComponentRepositoryInterface
interface AjaxControllerInterface
```
- **AND** ejemplos incorrectos:
```php
interface IRenderer // prefijo I estilo C#
interface Renderer // sin sufijo
interface renderer_interface // snake_case
```
---
### Requirement: Nomenclatura de Metodos
Los nombres de metodo DEBEN seguir convencion camelCase.
#### Scenario: Metodos publicos
- **WHEN** se define un metodo publico
- **THEN** el nombre DEBE seguir camelCase
- **AND** DEBE comenzar con verbo que describe la accion
- **AND** ejemplos correctos:
```php
public function render(Component $component): string
public function getVisibilityClass(array $data): ?string
public function validateInput(string $input): bool
public function buildForm(string $componentId): string
```
- **AND** ejemplos incorrectos:
```php
public function Render() // PascalCase
public function get_visibility_class() // snake_case
public function visibility() // sin verbo
```
#### Scenario: Metodos privados
- **WHEN** se define un metodo privado
- **THEN** DEBE seguir camelCase (igual que publicos)
- **AND** ejemplos correctos:
```php
private function parseResponse(): array
private function validateInternal(): bool
private function generateCSS(array $data): string
```
#### Scenario: Metodos booleanos
- **WHEN** un metodo retorna un valor booleano
- **THEN** DEBE usar prefijo `is`, `has`, `can`, `should`
- **AND** ejemplos correctos:
```php
public function isEnabled(array $data): bool
public function hasPermission(string $capability): bool
public function canProcess(): bool
public function shouldShow(string $component): bool
```
- **AND** ejemplos incorrectos:
```php
public function enabled(): bool // sin prefijo
public function checkEnabled(): bool // check no es booleano
public function getIsEnabled(): bool // get redundante
```
#### Scenario: Metodos getter/setter
- **WHEN** se define un metodo de acceso
- **THEN** getters DEBEN usar prefijo `get`
- **AND** setters DEBEN usar prefijo `set`
- **AND** ejemplos correctos:
```php
public function getData(): array
public function setData(array $data): void
public function getComponentName(): string
```
---
### Requirement: Nomenclatura de Propiedades y Variables
Las propiedades y variables DEBEN seguir convencion camelCase.
#### Scenario: Propiedades de clase
- **WHEN** se declara una propiedad de clase
- **THEN** DEBE seguir camelCase
- **AND** DEBE tener visibilidad explicita (private, protected, public)
- **AND** ejemplos correctos:
```php
private CSSGeneratorInterface $cssGenerator;
private string $componentName;
protected array $settings;
```
- **AND** ejemplos incorrectos:
```php
private $CssGenerator; // PascalCase
private $css_generator; // snake_case
private $_cssGenerator; // prefijo _ (no necesario en PHP moderno)
```
#### Scenario: Propiedades booleanas
- **WHEN** se define una propiedad booleana
- **THEN** DEBE usar prefijo `is`, `has`, `can`
- **AND** ejemplos correctos:
```php
private bool $isEnabled;
private bool $hasChanges;
private bool $canEdit;
```
#### Scenario: Variables locales
- **WHEN** se declara una variable local
- **THEN** DEBE seguir $camelCase
- **AND** DEBE ser descriptiva
- **AND** ejemplos correctos:
```php
$showDesktop = $data['visibility']['show_on_desktop'] ?? true;
$visibilityClass = $this->getVisibilityClass($data);
$componentSettings = $this->repository->get($componentName);
```
- **AND** ejemplos incorrectos:
```php
$ShowDesktop // PascalCase
$show_desktop // snake_case
$sd // abreviatura
$strShowDesktop // notacion hungara
```
#### Scenario: Parametros de metodo
- **WHEN** se declara un parametro de metodo
- **THEN** DEBE seguir $camelCase
- **AND** ejemplos correctos:
```php
public function render(Component $component): string
public function validateField(string $fieldName, mixed $value): bool
```
#### Scenario: Variables de iteracion
- **WHEN** se usa una variable de iteracion en bucle
- **THEN** se permiten nombres cortos para indices: `$i`, `$j`, `$k`
- **AND** se prefiere nombre descriptivo para elementos
- **AND** ejemplos correctos:
```php
for ($i = 0; $i < count($items); $i++)
foreach ($components as $component)
foreach ($settings as $key => $value)
```
---
### Requirement: Nomenclatura de Constantes
Las constantes DEBEN seguir convencion UPPER_SNAKE_CASE.
#### Scenario: Constantes de clase
- **WHEN** se define una constante de clase
- **THEN** DEBE seguir UPPER_SNAKE_CASE
- **AND** ejemplos correctos:
```php
private const COMPONENT_NAME = 'contact-form';
public const MAX_RETRY_COUNT = 3;
protected const DEFAULT_TIMEOUT = 5000;
```
- **AND** ejemplos incorrectos:
```php
private const componentName = 'contact-form'; // camelCase
private const ComponentName = 'contact-form'; // PascalCase
```
#### Scenario: Constantes globales
- **WHEN** se define una constante global del tema
- **THEN** DEBE usar prefijo `ROI_THEME_`
- **AND** DEBE seguir UPPER_SNAKE_CASE
- **AND** ejemplos correctos:
```php
define('ROI_THEME_VERSION', '1.0.0');
define('ROI_THEME_PATH', get_template_directory());
```
---
### Requirement: Nomenclatura de component_name
El identificador de componente DEBE usar kebab-case.
#### Scenario: component_name en JSON
- **WHEN** se define component_name en un schema JSON
- **THEN** DEBE usar kebab-case
- **AND** DEBE coincidir con el nombre del archivo JSON
- **AND** ejemplos correctos:
```json
{
"component_name": "contact-form",
"component_name": "featured-image",
"component_name": "top-notification-bar",
"component_name": "cta-box-sidebar"
}
```
#### Scenario: component_name en BD
- **WHEN** se guarda component_name en base de datos
- **THEN** DEBE mantener kebab-case
- **AND** tabla: `wp_roi_theme_component_settings`
- **AND** columna: `component_name`
#### Scenario: component_name en codigo PHP
- **WHEN** se usa component_name en codigo PHP
- **THEN** DEBE mantenerse en kebab-case
- **AND** ejemplos correctos:
```php
private const COMPONENT_NAME = 'contact-form';
public function supports(string $componentType): bool
{
return $componentType === 'contact-form';
}
// En data-attribute
$html .= 'data-component="contact-form"';
```
#### Scenario: Conversion kebab-case a PascalCase
- **WHEN** se necesita convertir component_name a nombre de carpeta/clase
- **THEN** eliminar guiones y capitalizar cada palabra
- **AND** ejemplos de conversion:
| kebab-case | PascalCase |
|------------|------------|
| `contact-form` | `ContactForm` |
| `featured-image` | `FeaturedImage` |
| `top-notification-bar` | `TopNotificationBar` |
| `cta-box-sidebar` | `CtaBoxSidebar` |
| `cta-lets-talk` | `CtaLetsTalk` |
---
### Requirement: Nomenclatura de Hooks WordPress
Los hooks DEBEN usar snake_case con prefijo del tema.
#### Scenario: Actions del tema
- **WHEN** se define un action hook del tema
- **THEN** DEBE usar prefijo `roi_theme_`
- **AND** DEBE seguir snake_case
- **AND** ejemplos correctos:
```php
do_action('roi_theme_after_render', $component);
do_action('roi_theme_before_form_submit');
do_action('roi_theme_component_loaded', $componentName);
```
#### Scenario: Filters del tema
- **WHEN** se define un filter hook del tema
- **THEN** DEBE usar prefijo `roi_theme_filter_`
- **AND** DEBE seguir snake_case
- **AND** ejemplos correctos:
```php
$css = apply_filters('roi_theme_filter_component_css', $css, $component);
$html = apply_filters('roi_theme_filter_render_output', $html);
```
#### Scenario: Acciones AJAX
- **WHEN** se registra una accion AJAX
- **THEN** DEBE usar prefijo `roi_`
- **AND** DEBE seguir snake_case
- **AND** ejemplos correctos:
```php
add_action('wp_ajax_roi_newsletter_subscribe', [$this, 'handle']);
add_action('wp_ajax_nopriv_roi_contact_form_submit', [$this, 'handle']);
```
---
### Requirement: Prohibicion de Notacion Hungara
La notacion hungara esta PROHIBIDA.
#### Scenario: Prefijos de tipo prohibidos
- **WHEN** se nombra una variable, propiedad o parametro
- **THEN** NO DEBE usar prefijos de tipo
- **AND** prefijos prohibidos:
| Prefijo | Significado | Ejemplo Incorrecto |
|---------|-------------|-------------------|
| `str` | String | `$strNombre` |
| `int`, `i` | Integer | `$intContador`, `$iTotal` |
| `b`, `bln` | Boolean | `$bActivo`, `$blnValido` |
| `arr` | Array | `$arrItems` |
| `obj` | Object | `$objRenderer` |
#### Scenario: Nombres correctos sin notacion hungara
- **WHEN** se reemplaza notacion hungara
- **THEN** usar nombres descriptivos
| Incorrecto | Correcto |
|------------|----------|
| `$strNombre` | `$name` o `$customerName` |
| `$bActivo` | `$isActive` |
| `$arrItems` | `$items` |
| `$objRenderer` | `$renderer` |
| `$intCount` | `$count` o `$itemCount` |
---
### Requirement: Validacion Pre-Commit de Nomenclatura
Las convenciones DEBEN validarse antes del commit.
#### Scenario: Checklist de nomenclatura
- **WHEN** el codigo esta listo para commit
- **THEN** verificar:
- [ ] Carpetas de modulo en PascalCase
- [ ] Archivos PHP en PascalCase.php
- [ ] Archivos JSON schema en kebab-case.json
- [ ] Namespaces en PascalCase jerarquico
- [ ] Clases en PascalCase
- [ ] Interfaces con sufijo Interface
- [ ] Metodos en camelCase
- [ ] Propiedades en camelCase
- [ ] Variables locales en $camelCase
- [ ] Constantes en UPPER_SNAKE_CASE
- [ ] component_name en kebab-case
- [ ] Hooks con prefijo roi_theme_
- [ ] SIN notacion hungara
- [ ] Metodos booleanos con is/has/can/should
- [ ] Renderers con sufijo Renderer
- [ ] FormBuilders con sufijo FormBuilder
- [ ] UseCases con sufijo UseCase
- [ ] Services con sufijo Service
---
## Tabla de Conversion Rapida
| De | A | Ejemplo |
|----|---|---------|
| kebab-case | PascalCase | `contact-form` → `ContactForm` |
| PascalCase | kebab-case | `ContactForm` → `contact-form` |
| PascalCase | camelCase | `ContactForm` → `contactForm` |
| snake_case | PascalCase | `contact_form` → `ContactForm` |
| snake_case | camelCase | `contact_form` → `contactForm` |
---
**Última actualización:** 2026-01-08

View File

@@ -0,0 +1,177 @@
# ⚙️ ESPECIFICACIONES DEL SISTEMA
## Requerimientos Generales
- **Persistencia de Datos**: Archivos JSON (NO localStorage, NO base de datos)
- **Vista Previa**: TODAS las pestañas DEBEN tener vista previa en tiempo real
- **Panel Principal**: Existe un panel de administración que lista todas las pestañas disponibles
- **Arquitectura**: Componentes independientes pero con diseño consistente
- **Navegación**: Sistema de pestañas/tabs para alternar entre componentes
---
## Stack Tecnológico
### CDN y Librerías
```html
<!-- Bootstrap 5.3.2 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/Css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons 1.11.3 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/Font/bootstrap-icons.min.css">
<!-- Google Fonts: Poppins -->
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
<!-- CSS del proyecto -->
<link rel="stylesheet" href="../../Css/style.css">
```
### JavaScript
```html
<!-- Bootstrap JS Bundle -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/Js/bootstrap.bundle.min.js"></script>
```
---
## Estructura de Archivos
**Arquitectura modular:** Cada componente tiene sus propios archivos separados (PHP, CSS, JS).
```
wp-content/themes/apus-theme/
└── admin-panel/
└── Admin/
├── Components/
│ ├── component-[name].php
│ ├── component-[name]-2.php
│ └── component-[name]-3.php
├── Assets/
│ ├── Css/
│ │ ├── component-[name].css
│ │ ├── component-[name]-2.css
│ │ └── component-[name]-3.css
│ └── Js/
│ ├── component-[name].js
│ ├── component-[name]-2.js
│ └── component-[name]-3.js
└── Config/
├── [name]-config.json
├── [name]-2-config.json
└── [name]-3-config.json
```
**Ejemplo con un componente específico:**
```
admin-panel/Admin/
├── Components/
│ └── component-notification-bar.php
├── Assets/
│ ├── Css/
│ │ └── component-notification-bar.css
│ └── Js/
│ └── component-notification-bar.js
└── Config/
└── notification-bar-config.json
```
---
## Convenciones de Nombrado
### Archivos PHP (Componentes)
- Patrón: `component-[nombre].php`
- Ubicación: `admin-panel/Admin/Components/`
- Ejemplo: `component-notification-bar.php`, `component-site-footer.php`
### Archivos CSS
- Patrón: `component-[nombre].css`
- Ubicación: `admin-panel/Admin/Assets/Css/`
- Mismo nombre base que el componente PHP correspondiente
### Archivos JavaScript
- Patrón: `component-[nombre].js`
- Ubicación: `admin-panel/Admin/Assets/Js/`
- Mismo nombre base que el componente PHP correspondiente
### Archivos de Configuración JSON
- Patrón: `[nombre]-config.json`
- Ubicación: `admin-panel/Admin/Config/`
- Ejemplo: `notification-bar-config.json`
---
## Dependencias del Proyecto
| Dependencia | Versión | Propósito |
|-------------|---------|-----------|
| Bootstrap | 5.3.2 | Framework CSS y componentes UI |
| Bootstrap Icons | 1.11.3 | Sistema de iconos |
| Poppins Font | Google Fonts | Tipografía principal |
| style.css | Custom | Estilos específicos del proyecto |
---
## Requerimientos del Navegador
- **Navegadores modernos**: Chrome, Firefox, Safari, Edge (últimas 2 versiones)
- **JavaScript**: Habilitado
- **CSS Grid**: Soporte requerido
- **Flexbox**: Soporte requerido
- **CSS Custom Properties**: Soporte requerido
---
## Estructura de un Componente
Cada componente debe tener:
1.**Archivo PHP** (`component-[name].php`)
- Formulario de configuración
- Vista previa en tiempo real
- Integración con WordPress
2.**Archivo CSS** (`component-[name].css`)
- Estilos específicos del componente admin
- Estilos para vista previa
3.**Archivo JavaScript** (`component-[name].js`)
- Funcionalidad del componente
- Event listeners
- Funciones de actualización de preview
- Persistencia de configuración
4.**Configuración JSON** (`[name]-config.json`)
- Valores por defecto
- Metadata del componente
- Timestamp de última modificación
---
## Reglas de Integración
### ❌ NO Permitido
- localStorage para persistencia (usar config.json)
- Base de datos directa
- Inline styles que sobreescriban CSS del front-end
- Modificar HTML del front-end desde admin
### ✅ Permitido
- Archivos JSON para configuración
- Inline styles SOLO en el admin panel (si no afecta preview)
- CSS con `!important` SOLO si es necesario para override de WordPress
---
## Volver al Índice
[← Volver al README](README.md)

View File

@@ -0,0 +1,199 @@
# 🎯 FILOSOFÍA DE DISEÑO
## Principios Clave
### 1. Consistencia Visual
Todos los componentes deben verse parte del mismo sistema.
**Implementación:**
- Paleta de colores unificada (Navy + Orange)
- Tipografía consistente (Poppins)
- Iconos del mismo sistema (Bootstrap Icons)
- Espaciado coherente entre componentes
### 2. Espaciado Compacto
Diseño eficiente que maximiza el espacio sin sacrificar usabilidad.
**Implementación:**
- Uso de `.form-control-sm` y `.btn-sm`
- Padding ajustado en cards (`.p-3`)
- Margins reducidos entre campos (`.mb-2`)
- Grid compacto con `.g-3`
### 3. Jerarquía Clara
Uso de colores, tamaños y pesos para guiar la atención.
**Jerarquía visual:**
1. Header del Tab (Navy gradient + Orange icon)
2. Títulos de Card (Navy + Bold)
3. Labels de Campos (Neutral + Semibold + Orange icon)
4. Texto de ayuda (Muted + Small)
### 4. Feedback Instantáneo
Vista previa en tiempo real de todos los cambios.
**Implementación:**
- Event listeners en todos los campos
- Función `updatePreview()` conectada
- Preview con HTML idéntico al front-end
- Sin delay en la actualización
### 5. Mobile-First
Diseño responsive que funciona en todos los dispositivos.
**Implementación:**
- Grid con breakpoint `col-lg-6`
- Stack vertical en mobile (<992px)
- Headers responsive con `flex-wrap`
- Botones full-width en mobile
---
## Características Comunes
Todos los componentes admin DEBEN incluir:
-**Vista previa en tiempo real**
- Card con border-left orange
- HTML idéntico al front-end
- Botones Desktop/Mobile
-**Validación de formularios**
- Campos requeridos marcados con `*`
- Validación en JavaScript
- Mensajes de error claros
-**Contador de caracteres en campos de texto**
- Display: `<span id="fieldCount">0</span>/250`
- Progress bar orange
- Actualización en tiempo real
-**Botón de reseteo a valores por defecto**
- En el header del tab
- Confirmación con `confirm()`
- Restaura todos los campos
-**Tooltips informativos**
- Links a documentación externa
- Hints con `<small class="text-muted">`
- Badges para información adicional
-**Iconos descriptivos en cada campo**
- Color orange (#FF8600)
- Bootstrap Icons
- Clase `.me-2` para espaciado
-**Feedback visual inmediato**
- Cambios reflejados sin delay
- Console.logs informativos
- Notificaciones de guardado
---
## Flujo de Interacción
### 1. Carga Inicial
```
Usuario abre admin panel
loadConfig() carga valores guardados
updatePreview() renderiza preview
Panel listo para edición
```
### 2. Edición de Campo
```
Usuario modifica campo
Event listener detecta cambio
updatePreview() actualiza preview
saveConfig() guarda en localStorage (opcional)
```
### 3. Guardado Final
```
Usuario confirma cambios (si hay botón guardar)
validateForm() verifica datos
saveConfig() guarda en config.json
showNotification() confirma guardado
```
### 4. Reset
```
Usuario presiona "Restaurar valores por defecto"
confirm() pide confirmación
resetToDefaults() aplica valores default
updatePreview() actualiza preview
saveConfig() guarda cambios
```
---
## Principios de Accesibilidad
### Labels y Formularios
- Todos los inputs tienen `<label for="id">`
- Labels descriptivos y claros
- Campos requeridos marcados visualmente
### Navegación por Teclado
- Tab order lógico
- Focus visible en elementos interactivos
- Enter para submit (si aplica)
### Contraste de Color
- Texto principal: #495057 sobre fondo blanco (AAA)
- Texto en headers: Blanco sobre navy (#0E2337) (AAA)
- Links: Orange (#FF8600) con hover (#FF6B35)
### ARIA Attributes
- Progress bars con `role="progressbar"`, `aria-valuenow`, etc.
- Button groups con `role="group"`
- Inputs con `aria-label` o `<label>`
---
## Mensajes y Comunicación
### Console.logs
```javascript
// ✅ USAR para debugging
console.log('✅ [ComponentName] Admin Panel cargado');
console.log('💾 Configuración guardada:', config);
console.log('📂 Configuración cargada:', config);
console.log('🔄 Valores por defecto restaurados');
// ❌ NO USAR en producción excesivamente
console.log('Campo actualizado'); // Demasiado verboso
```
### Notificaciones al Usuario
```javascript
// Success
showNotification('Cambios guardados', 'success');
// Error
showNotification('Error al guardar cambios', 'error');
// Confirmaciones
if (!confirm('¿Estás seguro de restaurar los valores por defecto?')) {
return;
}
```
---
## Volver al Índice
[← Volver al README](README.md)

View File

@@ -0,0 +1,228 @@
# 🎨 PALETA DE COLORES
## Colores Principales
### Navy Brand Colors
```css
--color-navy-dark: #0E2337; /* Fondo principal, headers */
--color-navy-primary: #1e3a5f; /* Títulos, bordes importantes */
--color-navy-light: #2c5282; /* Variaciones secundarias */
```
**Uso:**
- **#0E2337**: Fondo de gradientes, headers principales
- **#1e3a5f**: Títulos de cards, border-left de cards
- **#2c5282**: Variaciones y estados hover (opcional)
### Orange Accent Colors
```css
--color-orange-primary: #FF8600; /* Acción primaria, iconos destacados */
--color-orange-hover: #FF6B35; /* Hover states */
--color-orange-light: #FFB800; /* Badges, alerts suaves */
```
**Uso:**
- **#FF8600**: Iconos, border-left de preview, botones primarios
- **#FF6B35**: Hover en botones y links
- **#FFB800**: Badges informativos, highlights
### Neutral Colors
```css
--color-neutral-50: #f8f9fa; /* Fondo general del body */
--color-neutral-100: #e9ecef; /* Bordes, separadores */
--color-neutral-600: #495057; /* Texto principal */
--color-neutral-700: #6c757d; /* Texto secundario */
```
**Uso:**
- **#f8f9fa**: Background del body
- **#e9ecef**: Bordes sutiles, separadores
- **#495057**: Texto de labels y contenido principal
- **#6c757d**: Texto secundario, hints
---
## Uso de Colores por Elemento
### Tabla de Referencia
| Elemento | Color | Uso |
|----------|-------|-----|
| **Header del Tab** | Gradiente `#0E2337``#1e3a5f` | Encabezado principal de cada pestaña |
| **Títulos de Card** | `#1e3a5f` | Títulos de secciones dentro de cards |
| **Iconos Principales** | `#FF8600` | Todos los iconos destacados |
| **Bordes Importantes** | `#1e3a5f` (izquierda 4px) | Border-left de cards |
| **Bordes Especiales** | `#FF8600` (izquierda 4px) | Cards de vista previa |
| **Texto Labels** | `#495057` | Labels de formularios |
| **Badges** | `#FFB800` (fondo) + `#000` (texto) | Badges informativos |
| **Links** | `#FF8600` | Enlaces de ayuda |
| **Links Hover** | `#FF6B35` | Estado hover de enlaces |
| **Botón Primario** | `#FF8600` | Acciones principales |
| **Botón Primario Hover** | `#FF6B35` | Hover en botón primario |
| **Fondo Body** | `#f8f9fa` | Fondo general de la página |
---
## Ejemplos de Implementación
### Header del Tab
```html
<div class="rounded p-4 mb-4 shadow text-white"
style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%);
border-left: 4px solid #FF8600;">
<h3 class="h4 mb-1 fw-bold">
<i class="bi bi-megaphone-fill me-2" style="color: #FF8600;"></i>
Título del Componente
</h3>
</div>
```
### Card Estándar
```html
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
<div class="card-body">
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
<i class="bi bi-palette me-2" style="color: #FF8600;"></i>
Título de Sección
</h5>
<!-- Contenido -->
</div>
</div>
```
### Card de Vista Previa
```html
<div class="card shadow-sm mb-3" style="border-left: 4px solid #FF8600;">
<div class="card-body">
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
<i class="bi bi-eye me-2" style="color: #FF8600;"></i>
Vista Previa en Tiempo Real
</h5>
<!-- Preview -->
</div>
</div>
```
### Label con Icono
```html
<label for="fieldId" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-paint-bucket me-1" style="color: #FF8600;"></i>
Nombre del Campo
</label>
```
### Badge Informativo
```html
<span class="badge text-dark" style="background-color: #FFB800; font-size: 0.65rem;">
Opcional
</span>
```
### Link de Ayuda
```html
<small class="text-muted d-block mt-1">
<i class="bi bi-info-circle me-1"></i>
Ver: <a href="https://ejemplo.com" target="_blank"
class="text-decoration-none" style="color: #FF8600;">
Documentación <i class="bi bi-box-arrow-up-right"></i>
</a>
</small>
```
### Botón Primario
```css
.btn-primary {
background-color: #FF8600;
border-color: #FF8600;
color: #ffffff;
}
.btn-primary:hover {
background-color: #FF6B35;
border-color: #FF6B35;
}
```
---
## CSS con Variables
### Definición de Variables
```css
:root {
/* Navy Colors */
--navy-dark: #0E2337;
--navy-primary: #1e3a5f;
--navy-light: #2c5282;
/* Orange Colors */
--orange-primary: #FF8600;
--orange-hover: #FF6B35;
--orange-light: #FFB800;
/* Neutral Colors */
--neutral-50: #f8f9fa;
--neutral-100: #e9ecef;
--neutral-600: #495057;
--neutral-700: #6c757d;
}
```
### Uso de Variables
```css
/* Header del Tab */
.tab-header {
background: linear-gradient(135deg, var(--navy-dark) 0%, var(--navy-primary) 100%);
border-left: 4px solid var(--orange-primary);
}
/* Iconos */
.tab-header i,
.card-title i,
label i {
color: var(--orange-primary);
}
/* Títulos de Card */
.card-title {
color: var(--navy-primary);
}
```
---
## Contraste y Accesibilidad
### Ratios de Contraste (WCAG)
| Combinación | Ratio | Nivel WCAG |
|-------------|-------|------------|
| `#0E2337` sobre blanco | 12.6:1 | AAA |
| `#1e3a5f` sobre blanco | 8.2:1 | AAA |
| `#FF8600` sobre blanco | 3.4:1 | AA (large text) |
| Blanco sobre `#0E2337` | 12.6:1 | AAA |
| `#495057` sobre blanco | 7.8:1 | AAA |
**Recomendaciones:**
- ✅ Texto principal: `#495057` sobre blanco
- ✅ Texto en headers: Blanco sobre `#0E2337`
- ✅ Links: `#FF8600` (usar bold o underline para mejor accesibilidad)
- ✅ Títulos: `#1e3a5f` sobre blanco
---
## Volver al Índice
[← Volver al README](README.md)

View File

@@ -0,0 +1,336 @@
# 📝 TIPOGRAFÍA
## Font Stack
```css
font-family: 'Poppins', sans-serif;
```
**Carga desde Google Fonts:**
```html
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
```
---
## Pesos y Tamaños
### Headers
```css
/* Título principal del tab */
.h3, h3 {
font-size: 1.5rem; /* 24px */
font-weight: 700; /* Bold */
}
/* Subtítulos grandes */
.h4, h4 {
font-size: 1.25rem; /* 20px */
font-weight: 700; /* Bold */
}
/* Títulos de cards */
.h5, h5 {
font-size: 1rem; /* 16px */
font-weight: 700; /* Bold */
}
```
### Body Text
```css
/* Labels, descripciones */
.small {
font-size: 0.875rem; /* 14px */
}
/* Hints, contadores */
.text-muted {
font-size: 0.75rem; /* 12px */
}
```
### Font Weights
```css
.fw-bold {
font-weight: 700; /* Títulos principales */
}
.fw-semibold {
font-weight: 600; /* Labels importantes */
}
.fw-normal {
font-weight: 400; /* Texto normal */
}
```
---
## Jerarquía Visual
### Nivel 1: Título del Tab
```html
<h3 class="h4 mb-1 fw-bold" style="color: #0E2337;">
<i class="bi bi-megaphone-fill me-2" style="color: #FF8600;"></i>
Configuración del Top Bar
</h3>
```
**Características:**
- Color: Navy Dark (#0E2337) o Blanco (en gradiente)
- Peso: Bold (700)
- Tamaño: 1.25rem (h4 en h3)
- Icono: Orange (#FF8600)
### Nivel 2: Título de Card
```html
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
<i class="bi bi-palette me-2" style="color: #FF8600;"></i>
Colores y Estilos
</h5>
```
**Características:**
- Color: Navy Primary (#1e3a5f)
- Peso: Bold (700)
- Tamaño: 1rem
- Icono: Orange (#FF8600)
### Nivel 3: Label de Campo
```html
<label for="fieldId" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-paint-bucket me-1" style="color: #FF8600;"></i>
Color de Fondo
</label>
```
**Características:**
- Color: Neutral 600 (#495057)
- Peso: Semibold (600)
- Tamaño: 0.875rem (small)
- Icono: Orange (#FF8600)
### Nivel 4: Texto de Ayuda
```html
<small class="text-muted d-block mt-1">
<i class="bi bi-info-circle me-1"></i>
Este campo es opcional
</small>
```
**Características:**
- Color: Muted (Bootstrap default)
- Peso: Normal (400)
- Tamaño: 0.75rem
- Display: Block
---
## Ejemplos de Uso
### Header del Tab Completo
```html
<div class="rounded p-4 mb-4 shadow text-white"
style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%);">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
<div>
<!-- Título principal -->
<h3 class="h4 mb-1 fw-bold">
<i class="bi bi-megaphone-fill me-2" style="color: #FF8600;"></i>
Configuración del Top Bar
</h3>
<!-- Descripción -->
<p class="mb-0 small" style="opacity: 0.85;">
Personaliza la barra de anuncios superior del sitio
</p>
</div>
<button type="button" class="btn btn-sm btn-outline-light">
<i class="bi bi-arrow-counterclockwise me-1"></i>
Restaurar valores por defecto
</button>
</div>
</div>
```
### Card con Título
```html
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
<div class="card-body">
<!-- Título de sección -->
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
<i class="bi bi-palette me-2" style="color: #FF8600;"></i>
Colores y Estilos
</h5>
<!-- Contenido del card -->
</div>
</div>
```
### Campo de Formulario Completo
```html
<div class="mb-2">
<!-- Label con icono -->
<label for="messageText" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-chat-text me-1" style="color: #FF8600;"></i>
Mensaje Principal <span class="text-danger">*</span>
<span class="float-end text-muted">
<span id="messageTextCount" class="fw-bold">0</span>/250
</span>
</label>
<!-- Input -->
<textarea id="messageText"
class="form-control form-control-sm"
rows="2"
maxlength="250"
required></textarea>
<!-- Hint -->
<small class="text-muted d-block mt-1">
<i class="bi bi-info-circle me-1"></i>
Este mensaje se mostrará en la barra superior del sitio
</small>
</div>
```
---
## Line Height y Espaciado
### Line Heights Recomendados
```css
/* Títulos */
h1, h2, h3, h4, h5, h6 {
line-height: 1.2;
}
/* Body text */
p, .form-label, .small {
line-height: 1.5;
}
/* Text muted (hints) */
.text-muted {
line-height: 1.4;
}
```
### Letter Spacing
```css
/* Títulos principales */
h3, .h3 {
letter-spacing: -0.01em;
}
/* Labels */
.form-label {
letter-spacing: 0;
}
/* Texto normal */
body {
letter-spacing: 0;
}
```
---
## Responsive Typography
### Desktop (≥992px)
```css
@media (min-width: 992px) {
.h3, h3 {
font-size: 1.5rem; /* 24px */
}
.h4, h4 {
font-size: 1.25rem; /* 20px */
}
.h5, h5 {
font-size: 1rem; /* 16px */
}
}
```
### Tablet (576px - 991px)
```css
@media (max-width: 991px) {
.tab-header h3 {
font-size: 1.1rem; /* 17.6px */
}
}
```
### Mobile (<576px)
```css
@media (max-width: 575px) {
.tab-header h3 {
font-size: 1rem; /* 16px */
}
}
```
---
## Clases Utility de Tipografía
### Tamaños
```html
<p class="fs-1">Font size 1 (largest)</p>
<p class="fs-2">Font size 2</p>
<p class="fs-3">Font size 3</p>
<p class="fs-4">Font size 4</p>
<p class="fs-5">Font size 5</p>
<p class="fs-6">Font size 6 (smallest)</p>
```
### Pesos
```html
<p class="fw-bold">Bold text (700)</p>
<p class="fw-semibold">Semibold text (600)</p>
<p class="fw-normal">Normal weight (400)</p>
<p class="fw-light">Light weight (300)</p>
```
### Estilos
```html
<p class="fst-italic">Italic text</p>
<p class="fst-normal">Normal style</p>
<p class="text-decoration-underline">Underlined</p>
<p class="text-decoration-line-through">Strikethrough</p>
```
### Transformaciones
```html
<p class="text-lowercase">LOWERCASED TEXT</p>
<p class="text-uppercase">uppercased text</p>
<p class="text-capitalize">capitalized text</p>
```
---
## Volver al Índice
[← Volver al README](README.md)

View File

@@ -0,0 +1,340 @@
# 📏 SISTEMA DE GRID Y ESPACIADO
## Grid de Bootstrap
### Sistema de 12 Columnas
```css
.container-fluid {
max-width: 1400px; /* Máximo ancho del admin panel */
padding-right: var(--bs-gutter-x, 0.75rem);
padding-left: var(--bs-gutter-x, 0.75rem);
}
.row {
--bs-gutter-x: 1.5rem; /* Espacio horizontal entre columnas */
--bs-gutter-y: 0; /* Espacio vertical */
}
```
### Uso en Admin Panels
```css
.row.g-3 { /* Gap de 1rem (16px) */
--bs-gutter-x: 1rem;
--bs-gutter-y: 1rem;
}
.row.g-2 { /* Gap de 0.5rem (8px) */
--bs-gutter-x: 0.5rem;
--bs-gutter-y: 0.5rem;
}
```
---
## Breakpoints
| Breakpoint | Min Width | Max Width | Dispositivo | Comportamiento |
|------------|-----------|-----------|-------------|----------------|
| **xs** | 0px | 575px | Móvil | Stack vertical |
| **sm** | 576px | 767px | Tablet pequeña | Stack vertical |
| **md** | 768px | 991px | Tablet | Stack vertical |
| **lg** | 992px | 1199px | Desktop | 2 columnas |
| **xl** | 1200px | 1399px | Desktop grande | 2 columnas |
| **xxl** | 1400px | ∞ | Desktop XL | 2 columnas |
### Punto de Quiebre Principal: lg (992px)
```html
<!-- 2 columnas en desktop (≥992px), stack en mobile/tablet (<992px) -->
<div class="col-lg-6">Columna 1</div>
<div class="col-lg-6">Columna 2</div>
```
---
## Clases de Grid Comunes
### Layout de 2 Columnas (Admin Panel Estándar)
```html
<div class="row g-3">
<!-- Columna izquierda -->
<div class="col-lg-6">
<!-- Cards de configuración -->
</div>
<!-- Columna derecha -->
<div class="col-lg-6">
<!-- Cards de configuración -->
</div>
<!-- Fila completa (opcional) -->
<div class="col-12">
<!-- Vista previa o cards full-width -->
</div>
</div>
```
### Layout de 3 Columnas Desiguales
```html
<div class="row g-2 mb-2">
<div class="col-5">Campo 1</div>
<div class="col-5">Campo 2</div>
<div class="col-2">Campo 3</div>
</div>
```
### Layout de 2 Columnas Iguales (Campos de Color)
```html
<div class="row g-2 mb-2">
<div class="col-6">
<label>Color Fondo</label>
<input type="color" class="form-control form-control-color w-100">
</div>
<div class="col-6">
<label>Color Texto</label>
<input type="color" class="form-control form-control-color w-100">
</div>
</div>
```
### Full Width
```html
<div class="col-12">
<!-- Contenido full-width -->
</div>
```
---
## Sistema de Espaciado
### Escala de Espaciado (rem)
Bootstrap utiliza una escala basada en `rem`:
```css
/* Bootstrap Spacing Scale */
0 = 0
1 = 0.25rem (4px)
2 = 0.5rem (8px)
3 = 1rem (16px)
4 = 1.5rem (24px)
5 = 3rem (48px)
```
### Margin (m) y Padding (p)
#### Margin
```html
<!-- Margin Bottom -->
.mb-2 /* margin-bottom: 0.5rem (campos de formulario) */
.mb-3 /* margin-bottom: 1rem (cards, secciones) */
.mb-4 /* margin-bottom: 1.5rem (headers) */
<!-- Margin Right -->
.me-2 /* margin-right: 0.5rem (iconos) */
.me-1 /* margin-right: 0.25rem (iconos pequeños) */
<!-- Margin Left -->
.ms-2 /* margin-left: 0.5rem */
<!-- Margin Top -->
.mt-1 /* margin-top: 0.25rem (hints) */
.mt-3 /* margin-top: 1rem (separadores) */
<!-- Margin 0 (reset) -->
.m-0 /* margin: 0 */
.mb-0 /* margin-bottom: 0 */
```
#### Padding
```html
<!-- Padding uniforme -->
.p-4 /* padding: 1.5rem (header del tab) */
.p-3 /* padding: 1rem (card-body estándar) */
<!-- Padding vertical -->
.py-4 /* padding-top y bottom: 1.5rem (container principal) */
.py-3 /* padding-top y bottom: 1rem */
<!-- Padding horizontal -->
.px-3 /* padding-left y right: 1rem */
```
---
## Uso Estándar en Admin Components
### Tabla de Referencia
| Elemento | Clase | Valor Real | Uso |
|----------|-------|------------|-----|
| **Container Principal** | `.py-4` | 1.5rem (24px) | Padding vertical del contenedor |
| **Header del Tab** | `.mb-4` | 1.5rem (24px) | Separación después del header |
| **Cards** | `.mb-3` | 1rem (16px) | Separación entre cards |
| **Campos de Formulario** | `.mb-2` | 0.5rem (8px) | Separación entre campos |
| **Card Body** | `.p-3` | 1rem (16px) | Padding interno de cards |
| **Iconos en Título** | `.me-2` | 0.5rem (8px) | Espacio después de iconos grandes |
| **Iconos en Label** | `.me-1` | 0.25rem (4px) | Espacio después de iconos pequeños |
| **Títulos de Card** | `.mb-3` | 1rem (16px) | Separación después del título |
| **Hints/Small Text** | `.mt-1` | 0.25rem (4px) | Espacio antes de hints |
---
## Gap Utilities
### Flexbox/Grid Gap
```html
<!-- Gap en flex containers -->
<div class="d-flex gap-2"> /* 0.5rem (8px) entre elementos */
<div class="d-flex gap-3"> /* 1rem (16px) entre elementos */
<!-- Gap en grid (row) -->
<div class="row g-2"> /* 0.5rem (8px) entre columnas */
<div class="row g-3"> /* 1rem (16px) entre columnas */
```
### Ejemplo: Header Responsive con Gap
```html
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
<div>
<h3 class="h4 mb-1 fw-bold">Título</h3>
<p class="mb-0 small">Descripción</p>
</div>
<button class="btn btn-sm btn-outline-light">Botón</button>
</div>
```
**Qué hace:**
- `gap-3`: 1rem de espacio entre elementos hijos
- `flex-wrap`: Permite que el botón baje a la siguiente línea en mobile
- Mantiene espaciado consistente sin margins complejos
---
## Auto Layout
### Margin Auto
```html
<!-- Centrar horizontalmente -->
.mx-auto /* margin-left: auto; margin-right: auto; */
<!-- Empujar a la derecha -->
.ms-auto /* margin-left: auto; */
```
### Padding Auto (No existe en Bootstrap)
Bootstrap NO tiene padding auto. Usar flexbox para alineación:
```html
<div class="d-flex justify-content-between">
<div>Izquierda</div>
<div>Derecha</div>
</div>
```
---
## Ejemplos Completos
### Container Principal del Admin
```html
<div class="container-fluid py-4" style="max-width: 1400px;">
<!-- py-4: 1.5rem (24px) padding vertical -->
<!-- max-width: 1400px para no ser demasiado ancho -->
</div>
```
### Header del Tab
```html
<div class="rounded p-4 mb-4 shadow text-white"
style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%);">
<!-- p-4: 1.5rem (24px) padding uniforme -->
<!-- mb-4: 1.5rem (24px) margin-bottom -->
</div>
```
### Card Estándar
```html
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
<div class="card-body p-3">
<!-- mb-3: 1rem (16px) margin-bottom del card -->
<!-- p-3: 1rem (16px) padding del body -->
<h5 class="fw-bold mb-3">Título</h5>
<!-- mb-3: 1rem (16px) separación después del título -->
<div class="mb-2">
<!-- mb-2: 0.5rem (8px) separación entre campos -->
</div>
</div>
</div>
```
### Campo de Formulario con Label e Icono
```html
<div class="mb-2">
<label for="field" class="form-label small mb-1 fw-semibold">
<i class="bi bi-palette me-1" style="color: #FF8600;"></i>
<!-- me-1: 0.25rem (4px) después del icono -->
Nombre del Campo
</label>
<input type="text" id="field" class="form-control form-control-sm">
<small class="text-muted d-block mt-1">
<!-- mt-1: 0.25rem (4px) antes del hint -->
Texto de ayuda
</small>
</div>
```
---
## Responsive Spacing
### Desktop (≥992px)
Usar espaciado estándar sin modificaciones.
### Mobile (<576px)
```css
@media (max-width: 575px) {
/* Reducir padding en containers */
.container-fluid {
padding-right: 0.5rem;
padding-left: 0.5rem;
}
/* Reducir padding en headers */
.tab-header {
padding: 0.75rem !important;
}
/* Reducir padding en cards */
.card-body {
padding: 0.75rem !important;
}
}
```
---
## Volver al Índice
[← Volver al README](README.md)

View File

@@ -0,0 +1,366 @@
# 📐 ESTRUCTURA DE LAYOUT
## Container Principal
```html
<div class="container-fluid py-4" style="max-width: 1400px;">
<!-- Todo el contenido del admin panel va aquí -->
</div>
```
**Características:**
- `container-fluid`: Ancho flexible con padding
- `py-4`: 1.5rem (24px) padding vertical
- `max-width: 1400px`: Limita el ancho máximo para legibilidad
---
## Header del Tab (OBLIGATORIO EN TODOS LOS COMPONENTES)
### Estructura Completa
```html
<div class="rounded p-4 mb-4 shadow text-white"
style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%);
border-left: 4px solid #FF8600;">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
<div>
<h3 class="h4 mb-1 fw-bold">
<i class="bi bi-[ICON] me-2" style="color: #FF8600;"></i>
[TÍTULO DEL COMPONENTE]
</h3>
<p class="mb-0 small" style="opacity: 0.85;">
[Descripción breve del componente]
</p>
</div>
<button type="button" class="btn btn-sm btn-outline-light" id="reset[Component]Defaults">
<i class="bi bi-arrow-counterclockwise me-1"></i>
Restaurar valores por defecto
</button>
</div>
</div>
```
### Elementos Clave
-**Gradiente navy como fondo**: `#0E2337``#1e3a5f`
-**Border-left orange de 4px**: `#FF8600`
-**Icono orange en el título**: `color: #FF8600`
-**Botón de reset alineado a la derecha**
-**Responsive**: `flex-wrap` con `gap-3` para mobile
### Iconos Comunes por Tipo de Componente
| Tipo de Componente | Icono Sugerido | Clase Bootstrap Icons |
|--------------------|----------------|----------------------|
| Notificaciones/Anuncios | Megáfono | `bi-megaphone-fill` |
| Navegación/Menú | Barras | `bi-layout-text-window` |
| Colores/Estilos | Paleta | `bi-palette` |
| Textos/Contenido | Texto | `bi-chat-text` |
| Configuración General | Engranaje | `bi-gear-fill` |
| Imágenes/Media | Imagen | `bi-image` |
| Formularios | Portapapeles | `bi-clipboard-check` |
---
## Sistema de Grid
### Layout de 2 Columnas (Estándar)
```html
<div class="row g-3">
<!-- COLUMNA IZQUIERDA -->
<div class="col-lg-6">
<!-- Card 1 -->
<div class="card shadow-sm mb-3">...</div>
<!-- Card 2 -->
<div class="card shadow-sm mb-3">...</div>
</div>
<!-- COLUMNA DERECHA -->
<div class="col-lg-6">
<!-- Card 3 -->
<div class="card shadow-sm mb-3">...</div>
<!-- Card 4 -->
<div class="card shadow-sm mb-3">...</div>
</div>
<!-- FILA COMPLETA (opcional) -->
<div class="col-12">
<!-- Card de Vista Previa -->
<div class="card shadow-sm mb-3">...</div>
</div>
</div>
```
**Breakpoints:**
- `col-lg-6`: 2 columnas en pantallas ≥992px
- Stack vertical automático en pantallas <992px
- `g-3`: Gap de 1rem (16px) entre columnas
### Layout de 3 Columnas (Menos Común)
```html
<div class="row g-3">
<div class="col-lg-4">Card 1</div>
<div class="col-lg-4">Card 2</div>
<div class="col-lg-4">Card 3</div>
</div>
```
### Layout Full Width
```html
<div class="row g-3">
<div class="col-12">
<!-- Card de Vista Previa, Sección Especial -->
</div>
</div>
```
---
## Estructura Completa de un Admin Panel
```html
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin: [Component Name]</title>
<!-- CDN Links -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/Css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/Font/bootstrap-icons.min.css">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../../Css/style.css">
<style>
body {
font-family: 'Poppins', sans-serif;
background-color: #f8f9fa;
}
/* Sobreescribir max-width de WordPress */
body .card {
max-width: none !important;
}
</style>
</head>
<body>
<!-- CONTAINER PRINCIPAL -->
<div class="container-fluid py-4" style="max-width: 1400px;">
<!-- HEADER DEL TAB -->
<div class="rounded p-4 mb-4 shadow text-white"
style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%);
border-left: 4px solid #FF8600;">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
<div>
<h3 class="h4 mb-1 fw-bold">
<i class="bi bi-megaphone-fill me-2" style="color: #FF8600;"></i>
[TÍTULO DEL COMPONENTE]
</h3>
<p class="mb-0 small" style="opacity: 0.85;">
[Descripción breve]
</p>
</div>
<button type="button" class="btn btn-sm btn-outline-light" id="resetDefaults">
<i class="bi bi-arrow-counterclockwise me-1"></i>
Restaurar valores por defecto
</button>
</div>
</div>
<!-- GRID DE CONTENIDO -->
<div class="row g-3">
<!-- COLUMNA IZQUIERDA -->
<div class="col-lg-6">
<!-- Card de configuración 1 -->
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
<div class="card-body">
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
<i class="bi bi-palette me-2" style="color: #FF8600;"></i>
Sección 1
</h5>
<!-- Campos -->
</div>
</div>
</div>
<!-- COLUMNA DERECHA -->
<div class="col-lg-6">
<!-- Card de configuración 2 -->
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
<div class="card-body">
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
<i class="bi bi-gear me-2" style="color: #FF8600;"></i>
Sección 2
</h5>
<!-- Campos -->
</div>
</div>
</div>
<!-- VISTA PREVIA (Full Width) -->
<div class="col-12">
<div class="card shadow-sm mb-3" style="border-left: 4px solid #FF8600;">
<div class="card-body">
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
<i class="bi bi-eye me-2" style="color: #FF8600;"></i>
Vista Previa en Tiempo Real
</h5>
<!-- Preview del componente -->
<div id="componentPreview" class="[component-class]">
<!-- HTML idéntico al front-end -->
</div>
<div class="d-flex justify-content-between align-items-center mt-3">
<small class="text-muted">
<i class="bi bi-info-circle me-1"></i>
Los cambios se reflejan en tiempo real
</small>
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-secondary active" id="previewDesktop">
<i class="bi bi-display"></i> Desktop
</button>
<button type="button" class="btn btn-outline-secondary" id="previewMobile">
<i class="bi bi-phone"></i> Mobile
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/Js/bootstrap.bundle.min.js"></script>
<script>
// JavaScript del componente
document.addEventListener('DOMContentLoaded', function() {
console.log('✅ Admin Panel cargado');
loadConfig();
updatePreview();
initializeEventListeners();
});
</script>
</body>
</html>
```
---
## Cards de Configuración
### Card Estándar
```html
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
<div class="card-body">
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
<i class="bi bi-palette me-2" style="color: #FF8600;"></i>
[TÍTULO DE LA SECCIÓN]
</h5>
<!-- Campos de formulario -->
<div class="mb-2">
<!-- Campo 1 -->
</div>
<div class="mb-2">
<!-- Campo 2 -->
</div>
</div>
</div>
```
### Card de Vista Previa
```html
<div class="card shadow-sm mb-3" style="border-left: 4px solid #FF8600;">
<div class="card-body">
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
<i class="bi bi-eye me-2" style="color: #FF8600;"></i>
Vista Previa en Tiempo Real
</h5>
<!-- Preview -->
<div id="componentPreview" class="[component-class]">
<!-- HTML idéntico al front-end -->
</div>
<!-- Controles -->
<div class="d-flex justify-content-between align-items-center mt-3">
<small class="text-muted">
<i class="bi bi-info-circle me-1"></i>
Los cambios se reflejan en tiempo real
</small>
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-secondary active" id="previewDesktop">
<i class="bi bi-display"></i> Desktop
</button>
<button type="button" class="btn btn-outline-secondary" id="previewMobile">
<i class="bi bi-phone"></i> Mobile
</button>
</div>
</div>
</div>
</div>
```
**Diferencia clave:** Border-left ORANGE (#FF8600) en lugar de navy
---
## Responsive Behavior
### Desktop (≥992px)
- Grid de 2 columnas activo
- Header con layout horizontal
- Espaciado completo
### Tablet (768px - 991px)
- Stack vertical de columnas
- Header con layout horizontal (flex-wrap si es necesario)
- Espaciado completo
### Mobile (<768px)
- Stack vertical completo
- Header con botón debajo del título (flex-wrap)
- Espaciado reducido
```css
@media (max-width: 991px) {
.tab-header {
padding: 0.75rem;
}
.tab-header h3 {
font-size: 1.1rem;
}
}
@media (max-width: 575px) {
.tab-header h3 {
font-size: 1rem;
}
.card-body {
padding: 0.75rem !important;
}
}
```
---
## Volver al Índice
[← Volver al README](README.md)

View File

@@ -0,0 +1,92 @@
# 🧩 COMPONENTES REUTILIZABLES
## ¿Qué es esto?
Una **librería de 28 componentes HTML** listos para copiar y pegar, construidos con Bootstrap 5.3.2 y alineados al design system de APU.
Cada componente incluye:
- ✅ HTML completo y funcional
- ✅ Múltiples ejemplos de uso
- ✅ Comentarios explicativos inline
- ✅ Fixes para conflictos con WordPress
---
## 📂 Documentación Completa
**Ir a:** [`../componentes-html-bootstrap/README.md`](../componentes-html-bootstrap/README.md)
Ahí encontrarás:
- Índice completo de los 28 componentes
- Categorías organizadas (formularios, botones, layouts, navegación, etc.)
- Instrucciones de uso y personalización
- Dependencias requeridas (CDN links)
- Fixes CSS para WordPress
- Componentes que requieren JavaScript
---
## 📋 Categorías de Componentes
### 🎨 Layout y Estructura
Headers con gradiente, Cards (estándar y vista previa), Grids especializados
### 📝 Formularios
Switches, Color pickers, Text inputs, Textareas, Selects, Radio buttons, Checkboxes, Range sliders, File uploads
### 🔘 Botones
Botones simples, Button groups (Desktop/Mobile), Botones con dropdowns
### 🎯 Componentes Visuales
Badges, Links externos, Progress bars, Alerts, Spinners, Dividers, Tooltips
### 📱 Navegación y Organización
Tabs, Accordions, Sortable lists
### 💻 Desarrollo y Utilidades
Image previews, Code blocks
---
## 🚀 Cómo Usar
1. Abre el README de componentes: [`../componentes-html-bootstrap/README.md`](../componentes-html-bootstrap/README.md)
2. Encuentra el componente que necesitas
3. Abre el archivo HTML correspondiente (ej: `05-COLOR-PICKER.html`)
4. Copia el HTML del ejemplo que te sirva
5. Pega en tu proyecto y personaliza IDs/textos
---
## 🎨 Principios de Uso
### Colores
- **Iconos:** Siempre orange (`#FF8600`)
- **Títulos de card:** Siempre navy (`#1e3a5f`)
- **Border-left cards:** Navy para estándar, Orange para previews
- **Botones primarios:** Orange
### Tamaños
- **Form controls:** Usar `.form-control-sm` (tamaño compacto)
- **Botones en header:** Usar `.btn-sm`
- **Labels:** Clase `.small` + `fw-semibold`
### Patrones
- **Header de tab:** Gradiente navy + border-left orange + botón reset derecha
- **Vista previa:** Card con border-left orange + título "Vista Previa en Tiempo Real"
- **Iconos en labels:** `<i class="bi bi-[icon] me-1" style="color: #FF8600;"></i>`
---
## 📝 Notas Importantes
1. Los componentes HTML son la **fuente de verdad** - siempre usa el código de ahí
2. Todos los componentes están probados en WordPress
3. No mezclar inline styles con clases de Bootstrap (solo usa inline styles para colores de marca)
4. Siempre mantener accesibilidad (ARIA attributes incluidos)
---
## Volver al Índice
[← Volver al README](README.md)

View File

@@ -0,0 +1,443 @@
# 📝 PATRONES DE FORMULARIOS
## 1. Form Switch (Checkbox Toggle)
```html
<div class="mb-2">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="enabled" checked>
<label class="form-check-label small" for="enabled" style="color: #495057;">
<i class="bi bi-toggle-on me-1" style="color: #FF8600;"></i>
<strong>Activar Top Bar</strong>
<span class="text-muted">(Mostrar en el sitio)</span>
</label>
</div>
</div>
```
**Uso:** Activación/desactivación de features
**CSS necesario (Fix WordPress):**
```css
/* Eliminar pseudo-elementos de WordPress */
body .form-switch .form-check-input[type="checkbox"]::before,
body .form-switch .form-check-input[type="checkbox"]::after {
content: none !important;
display: none !important;
}
body .form-switch .form-check-input[type="checkbox"] {
background-size: contain !important;
background-repeat: no-repeat !important;
background-position: left center !important;
}
body .form-switch .form-check-input[type="checkbox"]:checked {
background-position: right center !important;
}
/* Fix alineación vertical */
.form-check.form-switch {
display: flex !important;
align-items: center !important;
}
.form-switch .form-check-input {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.form-switch .form-check-label {
line-height: 16px !important;
padding-top: 0 !important;
margin-top: 0 !important;
}
```
---
## 2. Color Picker
```html
<div class="col-6">
<label for="bgColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-paint-bucket me-1" style="color: #FF8600;"></i>
Color de Fondo
</label>
<input type="color" id="bgColor"
class="form-control form-control-color w-100"
value="#0E2337"
title="Seleccionar color de fondo">
<small class="text-muted d-block mt-1" id="bgColorValue">#0E2337</small>
</div>
```
**Características:**
- Grid de 2 columnas (`col-6`)
- Display del valor hex debajo
- Width 100% (`.w-100`)
**JavaScript:**
```javascript
const colorInput = document.getElementById('bgColor');
const colorValue = document.getElementById('bgColorValue');
colorInput.addEventListener('input', function() {
colorValue.textContent = this.value.toUpperCase();
updatePreview();
});
```
---
## 3. Text Input con Icono
```html
<div class="mb-2">
<label for="highlightText" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-chat-text me-1" style="color: #FF8600;"></i>
Texto Destacado
<span class="text-danger">*</span> <!-- Si es requerido -->
</label>
<input type="text" id="highlightText"
class="form-control form-control-sm"
placeholder="Ej: Nuevo:"
value="Nuevo:"
maxlength="50">
</div>
```
**Uso:** Campos de texto cortos (nombres, títulos, etc.)
---
## 4. Textarea con Contador y Progress Bar
```html
<div class="mb-2">
<label for="messageText" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-chat-left-text me-1" style="color: #FF8600;"></i>
Mensaje Principal <span class="text-danger">*</span>
<span class="float-end text-muted">
<span id="messageTextCount" class="fw-bold">0</span>/250
</span>
</label>
<textarea id="messageText"
class="form-control form-control-sm"
rows="2"
maxlength="250"
placeholder="Ej: Accede a más de 200,000 APUs actualizados"
required></textarea>
<div class="progress mt-1" style="height: 3px;">
<div id="messageTextProgress"
class="progress-bar"
role="progressbar"
style="width: 0%; background-color: #FF8600;"
aria-valuenow="0"
aria-valuemin="0"
aria-valuemax="250"></div>
</div>
</div>
```
**JavaScript:**
```javascript
const textarea = document.getElementById('messageText');
const counter = document.getElementById('messageTextCount');
const progress = document.getElementById('messageTextProgress');
textarea.addEventListener('input', function() {
const length = this.value.length;
const maxLength = 250;
const percentage = (length / maxLength) * 100;
counter.textContent = length;
progress.style.width = percentage + '%';
progress.setAttribute('aria-valuenow', length);
// Cambiar color según el uso
if (percentage > 90) {
progress.style.backgroundColor = '#dc3545'; // Rojo
} else if (percentage > 75) {
progress.style.backgroundColor = '#ffc107'; // Amarillo
} else {
progress.style.backgroundColor = '#FF8600'; // Orange
}
updatePreview();
});
```
---
## 5. Select Dropdown
```html
<div class="mb-2">
<label for="fontSize" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-fonts me-1" style="color: #FF8600;"></i>
Tamaño de Fuente
</label>
<select id="fontSize" class="form-select form-select-sm">
<option value="small">Pequeño (0.875rem)</option>
<option value="normal" selected>Normal (1rem)</option>
<option value="large">Grande (1.125rem)</option>
</select>
</div>
```
**Uso:** Opciones predefinidas (tamaños, estilos, etc.)
---
## 6. Badges Informativos
### Badge en Label
```html
<label class="form-label small mb-1 fw-semibold">
<i class="bi bi-code me-1" style="color: #FF8600;"></i>
Clase del Icono
<span class="badge bg-secondary" style="font-size: 0.65rem;">Bootstrap Icons</span>
</label>
```
### Badge Standalone
```html
<!-- Opcional -->
<span class="badge text-dark" style="background-color: #FFB800; font-size: 0.65rem;">
Opcional
</span>
<!-- Info -->
<span class="badge bg-secondary" style="font-size: 0.65rem;">
Info
</span>
<!-- Requerido -->
<span class="badge bg-danger" style="font-size: 0.65rem;">
Requerido
</span>
```
---
## 7. Links de Ayuda
```html
<small class="text-muted d-block mt-1">
<i class="bi bi-info-circle me-1"></i>
Ver: <a href="https://icons.getbootstrap.com/" target="_blank"
class="text-decoration-none" style="color: #FF8600;">
Bootstrap Icons <i class="bi bi-box-arrow-up-right"></i>
</a>
</small>
```
**Características:**
- Icono de info
- Link orange con hover
- Icono de "abrir en nueva ventana"
- `target="_blank"` para abrir en pestaña nueva
---
## 8. Grid de Inputs Compactos
### 2 Columnas Iguales
```html
<div class="row g-2 mb-2">
<div class="col-6">
<label for="bgColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-paint-bucket me-1" style="color: #FF8600;"></i>
Color Fondo
</label>
<input type="color" id="bgColor"
class="form-control form-control-color w-100"
value="#0E2337">
<small class="text-muted d-block mt-1" id="bgColorValue">#0E2337</small>
</div>
<div class="col-6">
<label for="textColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-fonts me-1" style="color: #FF8600;"></i>
Color Texto
</label>
<input type="color" id="textColor"
class="form-control form-control-color w-100"
value="#ffffff">
<small class="text-muted d-block mt-1" id="textColorValue">#FFFFFF</small>
</div>
</div>
```
### 3 Columnas Desiguales
```html
<div class="row g-2 mb-2">
<div class="col-5">
<label class="form-label small mb-1 fw-semibold">Campo 1</label>
<input type="text" class="form-control form-control-sm">
</div>
<div class="col-5">
<label class="form-label small mb-1 fw-semibold">Campo 2</label>
<input type="text" class="form-control form-control-sm">
</div>
<div class="col-2">
<label class="form-label small mb-1 fw-semibold">Icono</label>
<input type="text" class="form-control form-control-sm">
</div>
</div>
```
---
## 9. Campo de URL
```html
<div class="mb-2">
<label for="linkUrl" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-link-45deg me-1" style="color: #FF8600;"></i>
URL del Enlace
</label>
<input type="url" id="linkUrl"
class="form-control form-control-sm"
placeholder="https://ejemplo.com"
value="/catalogo">
<small class="text-muted d-block mt-1">
<i class="bi bi-info-circle me-1"></i>
Usa rutas relativas (/) o absolutas (https://)
</small>
</div>
```
---
## 10. Campos Relacionados (Link)
```html
<!-- Switch para mostrar/ocultar link -->
<div class="mb-2">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="showLink" checked>
<label class="form-check-label small" for="showLink" style="color: #495057;">
<i class="bi bi-link me-1" style="color: #FF8600;"></i>
<strong>Mostrar Enlace</strong>
<span class="text-muted">(Call-to-action)</span>
</label>
</div>
</div>
<!-- Campos del link (se muestran/ocultan según el switch) -->
<div id="linkFields">
<!-- Texto del link -->
<div class="mb-2">
<label for="linkText" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-chat-text me-1" style="color: #FF8600;"></i>
Texto del Enlace
</label>
<input type="text" id="linkText"
class="form-control form-control-sm"
placeholder="Ej: Ver Catálogo →"
value="Ver Catálogo →"
maxlength="50">
</div>
<!-- URL del link -->
<div class="mb-2">
<label for="linkUrl" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-link-45deg me-1" style="color: #FF8600;"></i>
URL del Enlace
</label>
<input type="url" id="linkUrl"
class="form-control form-control-sm"
placeholder="https://ejemplo.com"
value="/catalogo">
</div>
<!-- Target del link -->
<div class="mb-2">
<label for="linkTarget" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-box-arrow-up-right me-1" style="color: #FF8600;"></i>
Abrir En
</label>
<select id="linkTarget" class="form-select form-select-sm">
<option value="_self" selected>Misma ventana (_self)</option>
<option value="_blank">Nueva pestaña (_blank)</option>
</select>
</div>
</div>
```
**JavaScript para mostrar/ocultar:**
```javascript
const showLink = document.getElementById('showLink');
const linkFields = document.getElementById('linkFields');
showLink.addEventListener('change', function() {
linkFields.style.display = this.checked ? 'block' : 'none';
updatePreview();
});
```
---
## 11. Validación de Campos
```javascript
/**
* Valida los campos del formulario
*/
function validateForm() {
let isValid = true;
const errors = [];
// Validar campo requerido
const messageText = document.getElementById('messageText').value.trim();
if (!messageText) {
errors.push('El mensaje principal es requerido');
isValid = false;
}
// Validar longitud
if (messageText.length > 250) {
errors.push('El mensaje no puede exceder 250 caracteres');
isValid = false;
}
// Validar URL
const linkUrl = document.getElementById('linkUrl').value.trim();
if (linkUrl && !isValidUrl(linkUrl)) {
errors.push('La URL no es válida');
isValid = false;
}
if (!isValid) {
alert('⚠️ Errores de validación:\n\n' + errors.join('\n'));
}
return isValid;
}
function isValidUrl(string) {
// Permitir rutas relativas
if (string.startsWith('/')) {
return true;
}
// Validar URLs absolutas
try {
new URL(string);
return true;
} catch (_) {
return false;
}
}
```
---
## Volver al Índice
[← Volver al README](README.md)

View File

@@ -0,0 +1,360 @@
# 👁️ VISTA PREVIA EN TIEMPO REAL
## Estructura HTML del Preview
```html
<div class="col-12">
<div class="card shadow-sm mb-3" style="border-left: 4px solid #FF8600;">
<div class="card-body">
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
<i class="bi bi-eye me-2" style="color: #FF8600;"></i>
Vista Previa en Tiempo Real
</h5>
<!-- IMPORTANTE: Usar las MISMAS clases que el componente real -->
<div id="componentPreview" class="[clases-del-componente-real]">
<!-- HTML IDÉNTICO al del front-end -->
</div>
<div class="d-flex justify-content-between align-items-center mt-3">
<small class="text-muted">
<i class="bi bi-info-circle me-1"></i>
Los cambios se reflejan en tiempo real
</small>
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-secondary active" id="previewDesktop">
<i class="bi bi-display"></i> Desktop
</button>
<button type="button" class="btn btn-outline-secondary" id="previewMobile">
<i class="bi bi-phone"></i> Mobile
</button>
</div>
</div>
</div>
</div>
</div>
```
**Características clave:**
- ✅ Border-left ORANGE (#FF8600) - NO navy
- ✅ Clases IDÉNTICAS al componente del front-end
- ✅ HTML IDÉNTICO al front-end
- ✅ Botones Desktop/Mobile para cambiar el ancho
---
## Reglas Críticas para Vista Previa
### ❌ LO QUE NO DEBES HACER
#### 1. NO usar inline styles que sobreescriban el CSS real
```html
<!-- ❌ MAL: Inline styles que sobreescriben el CSS real -->
<div id="preview" class="component" style="padding: 10px; color: blue;">
<!-- Esto NO se verá igual al front-end -->
</div>
```
#### 2. NO crear CSS específico para el preview que no existe en el front-end
```css
/* ❌ MAL: CSS específico para el preview que no existe en el front-end */
#componentPreview {
padding: 20px;
font-size: 16px;
background-color: #f0f0f0;
}
```
#### 3. NO modificar la estructura HTML del componente
```html
<!-- ❌ MAL: HTML diferente al front-end -->
<div id="preview" class="my-custom-wrapper">
<div class="component">
<!-- Estructura diferente -->
</div>
</div>
```
---
### ✅ LO QUE DEBES HACER
#### 1. Usar EXACTAMENTE las mismas clases que el front-end
```html
<!-- ✅ BIEN: Usar EXACTAMENTE las mismas clases que el front-end -->
<div id="topBarPreview" class="top-notification-bar">
<div class="container">
<div class="notification-content">
<!-- Estructura IDÉNTICA al front-end -->
</div>
</div>
</div>
```
#### 2. Cargar el CSS del front-end en el admin
```html
<!-- ✅ BIEN: Cargar el CSS real del front-end -->
<link rel="stylesheet" href="../../Css/style.css">
```
#### 3. Solo aplicar estilos si es ABSOLUTAMENTE necesario con !important
```css
/* ✅ BIEN: Solo si el CSS del front-end no se aplica correctamente */
#componentPreview.top-notification-bar {
/* Solo agregar si es necesario para el contexto del admin */
border: 1px solid #e9ecef; /* Border para debugging visual */
}
```
---
## Ejemplo Completo: Top Bar Preview
### HTML del Front-end (Original)
```html
<div class="top-notification-bar">
<div class="container">
<div class="notification-content">
<i class="bi bi-megaphone-fill notification-icon"></i>
<span class="notification-text">
<strong class="notification-highlight">Nuevo:</strong>
Accede a más de 200,000 APUs actualizados.
</span>
<a href="/catalogo" class="notification-link">
Ver Catálogo →
</a>
</div>
</div>
</div>
```
### HTML del Preview (Admin) ✅ CORRECTO
```html
<!-- ✅ IDÉNTICO al front-end -->
<div id="topBarPreview" class="top-notification-bar">
<div class="container">
<div class="notification-content">
<i class="bi bi-megaphone-fill notification-icon" id="previewIcon"></i>
<span class="notification-text">
<strong class="notification-highlight" id="previewHighlight">Nuevo:</strong>
<span id="previewMessage">Accede a más de 200,000 APUs actualizados.</span>
</span>
<a href="/catalogo" class="notification-link" id="previewLink">
Ver Catálogo →
</a>
</div>
</div>
</div>
```
**Diferencias permitidas:**
- ✅ IDs agregados para manipulación con JavaScript (`id="previewIcon"`, `id="previewMessage"`, etc.)
- ✅ Clases CSS IDÉNTICAS al front-end
- ✅ Estructura HTML IDÉNTICA
---
## CSS para Vista Previa
### Principio: NO sobreescribir estilos del front-end
```css
/* REGLA DE ORO: NO sobreescribir estilos del front-end */
/* Solo agregar si es absolutamente necesario */
/* ✅ Permitido: Border para distinguir visualmente el preview en el admin */
#topBarPreview {
border: 1px solid #e9ecef;
border-radius: 0.375rem;
}
/* ❌ NO permitido: Sobreescribir propiedades del componente */
#topBarPreview {
padding: 20px !important; /* ❌ Esto hará que no se vea igual */
font-size: 18px !important; /* ❌ Esto hará que no se vea igual */
}
```
---
## JavaScript para updatePreview()
### Patrón Básico
```javascript
/**
* Actualiza la vista previa en tiempo real
*/
function updatePreview() {
const preview = document.getElementById('topBarPreview');
if (!preview) return;
// REGLA: Solo modificar propiedades que el usuario puede cambiar
// NO modificar padding, margins, o estructura del HTML
// 1. Actualizar colores
const bgColor = document.getElementById('bgColor').value;
const textColor = document.getElementById('textColor').value;
preview.style.backgroundColor = bgColor;
preview.style.color = textColor;
// 2. Actualizar texto
const highlightText = document.getElementById('highlightText').value;
const messageText = document.getElementById('messageText').value;
document.getElementById('previewHighlight').textContent = highlightText;
document.getElementById('previewMessage').textContent = messageText;
// 3. Actualizar link
const showLink = document.getElementById('showLink').checked;
const linkElement = document.getElementById('previewLink');
linkElement.style.display = showLink ? 'inline-block' : 'none';
if (showLink) {
const linkText = document.getElementById('linkText').value;
const linkUrl = document.getElementById('linkUrl').value;
const linkTarget = document.getElementById('linkTarget').value;
linkElement.textContent = linkText;
linkElement.href = linkUrl;
linkElement.target = linkTarget;
}
// 4. Mostrar/ocultar icono
const showIcon = document.getElementById('showIcon').checked;
const iconElement = document.getElementById('previewIcon');
iconElement.style.display = showIcon ? 'inline-block' : 'none';
// 5. Cambiar clase del icono
const iconClass = document.getElementById('iconClass').value;
iconElement.className = iconClass + ' notification-icon';
}
```
### Conectar updatePreview() a Todos los Campos
```javascript
function initializeEventListeners() {
// Lista de campos que deben actualizar el preview
const fields = [
'bgColor',
'textColor',
'highlightColor',
'highlightText',
'messageText',
'showLink',
'linkText',
'linkUrl',
'linkTarget',
'showIcon',
'iconClass',
'fontSize'
];
// Conectar event listeners
fields.forEach(fieldId => {
const element = document.getElementById(fieldId);
if (element) {
if (element.type === 'checkbox') {
element.addEventListener('change', updatePreview);
} else {
element.addEventListener('input', updatePreview);
}
}
});
}
```
---
## Botones Desktop/Mobile
### HTML
```html
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-secondary active" id="previewDesktop">
<i class="bi bi-display"></i> Desktop
</button>
<button type="button" class="btn btn-outline-secondary" id="previewMobile">
<i class="bi bi-phone"></i> Mobile
</button>
</div>
```
### JavaScript
```javascript
const btnDesktop = document.getElementById('previewDesktop');
const btnMobile = document.getElementById('previewMobile');
const preview = document.getElementById('topBarPreview');
btnDesktop.addEventListener('click', function() {
// Activar botón
this.classList.add('active');
btnMobile.classList.remove('active');
// Cambiar ancho del preview
preview.style.maxWidth = '100%';
preview.style.margin = '0';
});
btnMobile.addEventListener('click', function() {
// Activar botón
this.classList.add('active');
btnDesktop.classList.remove('active');
// Cambiar ancho del preview (simular mobile)
preview.style.maxWidth = '375px';
preview.style.margin = '0 auto';
});
```
---
## Checklist de Vista Previa
Antes de considerar completa la vista previa, verificar:
- [ ] HTML del preview es IDÉNTICO al front-end
- [ ] Se usan las MISMAS clases CSS que el componente real
- [ ] Se carga el archivo CSS del front-end (`../../Css/style.css`)
- [ ] NO hay inline styles que sobreescriban propiedades del componente
- [ ] La función `updatePreview()` está conectada a todos los campos
- [ ] Los botones Desktop/Mobile funcionan correctamente
- [ ] El preview se ve IDÉNTICO al componente en el sitio real
---
## Debugging de Vista Previa
### Problema: El preview no se ve igual al front-end
**Solución:**
1. Abrir DevTools (F12)
2. Inspeccionar el elemento del preview
3. Verificar que se cargan los estilos correctos:
```
Computed Styles →
padding: 0.5rem 0 (debe venir de style.css)
background-color: rgb(14, 35, 55) (debe venir del inline style del preview)
```
4. Si hay estilos incorrectos:
- Verificar que `style.css` esté cargado
- Verificar que las clases CSS sean idénticas
- Verificar que no haya inline styles conflictivos
---
## Volver al Índice
[← Volver al README](README.md)

View File

@@ -0,0 +1,422 @@
# 📱 RESPONSIVE DESIGN
## Breakpoints de Bootstrap
| Breakpoint | Min Width | Dispositivo | Uso en Admin Panel |
|------------|-----------|-------------|-------------------|
| **xs** | 0px | Móvil pequeño | Stack vertical, padding reducido |
| **sm** | 576px | Móvil grande | Stack vertical |
| **md** | 768px | Tablet | Stack vertical |
| **lg** | 992px | Desktop | Grid 2 columnas activo |
| **xl** | 1200px | Desktop grande | Grid 2 columnas |
| **xxl** | 1400px | Desktop XL | Grid 2 columnas |
---
## Media Queries
### Mobile (<576px)
```css
@media (max-width: 575px) {
/* Header del Tab: reducir tamaño de título */
.tab-header h3 {
font-size: 1rem;
}
/* Cards: reducir padding */
.form-section.card .card-body {
padding: 0.75rem !important;
}
/* Container: reducir padding lateral */
.container-fluid {
padding-right: 0.5rem;
padding-left: 0.5rem;
}
/* Botones: full width */
.tab-header button {
width: 100%;
margin-top: 0.5rem;
}
}
```
### Tablet (576px - 991px)
```css
@media (max-width: 991px) {
/* Header del Tab: reducir padding */
.tab-header {
padding: 0.75rem;
}
/* Título: tamaño medio */
.tab-header h3 {
font-size: 1.1rem;
}
/* Grid: todavía en stack vertical */
}
```
### Desktop (≥992px)
```css
@media (min-width: 992px) {
/* Grid de 2 columnas activo */
.col-lg-6 {
flex: 0 0 auto;
width: 50%;
}
/* Espaciado normal */
}
```
---
## Patrones Responsive
### Header del Tab
#### HTML Responsive
```html
<div class="rounded p-4 mb-4 shadow text-white"
style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%);">
<!-- flex-wrap permite que el botón baje en mobile -->
<!-- gap-3 mantiene espaciado consistente -->
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
<div>
<h3 class="h4 mb-1 fw-bold">Título</h3>
<p class="mb-0 small">Descripción</p>
</div>
<button class="btn btn-sm btn-outline-light">Botón</button>
</div>
</div>
```
#### CSS Responsive
```css
@media (max-width: 576px) {
/* El botón baja automáticamente con flex-wrap */
/* gap-3 mantiene el espaciado */
/* Opcional: hacer botón full-width */
.tab-header button {
width: 100%;
}
/* Reducir padding del header */
.tab-header {
padding: 0.75rem !important;
}
}
```
---
### Grid de 2 Columnas
#### HTML Responsive
```html
<div class="row g-3">
<!-- col-lg-6: 2 columnas en desktop (≥992px) -->
<!-- Stack vertical automático en mobile/tablet (<992px) -->
<div class="col-lg-6">
<!-- Cards de configuración -->
</div>
<div class="col-lg-6">
<!-- Cards de configuración -->
</div>
</div>
```
**Comportamiento:**
- **Desktop (≥992px)**: 2 columnas lado a lado
- **Mobile/Tablet (<992px)**: Stack vertical automático
- **Gap**: 1rem (16px) entre elementos
---
### Botones de Vista Previa
#### HTML Responsive
```html
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-secondary active">
<i class="bi bi-display"></i> Desktop
</button>
<button type="button" class="btn btn-outline-secondary">
<i class="bi bi-phone"></i> Mobile
</button>
</div>
```
#### CSS Responsive
```css
@media (max-width: 576px) {
/* Reducir tamaño de botones en mobile */
.btn-group.btn-group-sm .btn {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
}
/* Opcional: full width */
.btn-group.btn-group-sm {
width: 100%;
}
}
```
---
### Cards
#### HTML Responsive (Automático)
```html
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
<div class="card-body">
<!-- Contenido -->
</div>
</div>
```
#### CSS Responsive
```css
@media (max-width: 575px) {
/* Reducir padding en cards */
.card-body {
padding: 0.75rem !important;
}
/* Reducir margen entre cards */
.card {
margin-bottom: 0.75rem !important;
}
}
```
---
## Contenedor Principal
### HTML
```html
<div class="container-fluid py-4" style="max-width: 1400px;">
<!-- Contenido -->
</div>
```
### CSS Responsive
```css
/* Desktop: ancho máximo 1400px */
@media (min-width: 1400px) {
.container-fluid {
max-width: 1400px;
}
}
/* Tablet: ancho completo con padding */
@media (max-width: 991px) {
.container-fluid {
padding-right: 1rem;
padding-left: 1rem;
}
}
/* Mobile: padding reducido */
@media (max-width: 575px) {
.container-fluid {
padding-right: 0.5rem;
padding-left: 0.5rem;
}
}
```
---
## Vista Previa Responsive
### Simular Mobile en Preview
```javascript
const btnMobile = document.getElementById('previewMobile');
const preview = document.getElementById('componentPreview');
btnMobile.addEventListener('click', function() {
// Simular ancho mobile (375px - iPhone SE)
preview.style.maxWidth = '375px';
preview.style.margin = '0 auto';
// Opcional: agregar border para visualizar límites
preview.style.border = '1px solid #e9ecef';
});
```
### Simular Desktop en Preview
```javascript
const btnDesktop = document.getElementById('previewDesktop');
const preview = document.getElementById('componentPreview');
btnDesktop.addEventListener('click', function() {
// Restaurar ancho completo
preview.style.maxWidth = '100%';
preview.style.margin = '0';
// Remover border
preview.style.border = 'none';
});
```
---
## Anchos de Referencia para Dispositivos
| Dispositivo | Ancho (px) | Uso en Preview |
|-------------|------------|----------------|
| iPhone SE | 375 | Mobile pequeño |
| iPhone 12/13 | 390 | Mobile estándar |
| iPhone 14 Pro Max | 430 | Mobile grande |
| iPad Mini | 768 | Tablet |
| iPad Pro | 1024 | Tablet grande |
| Desktop | 1200+ | Desktop estándar |
---
## Tipografía Responsive
### Headers
```css
/* Desktop: tamaño completo */
@media (min-width: 992px) {
.tab-header h3 {
font-size: 1.5rem; /* 24px */
}
.card-title {
font-size: 1rem; /* 16px */
}
}
/* Tablet: tamaño reducido */
@media (max-width: 991px) {
.tab-header h3 {
font-size: 1.1rem; /* 17.6px */
}
}
/* Mobile: tamaño mínimo */
@media (max-width: 575px) {
.tab-header h3 {
font-size: 1rem; /* 16px */
}
.card-title {
font-size: 0.9rem; /* 14.4px */
}
}
```
---
## Espaciado Responsive
### Padding
```css
/* Desktop: padding completo */
@media (min-width: 992px) {
.tab-header {
padding: 1.5rem; /* 24px */
}
.card-body {
padding: 1rem; /* 16px */
}
}
/* Mobile: padding reducido */
@media (max-width: 575px) {
.tab-header {
padding: 0.75rem !important; /* 12px */
}
.card-body {
padding: 0.75rem !important; /* 12px */
}
}
```
---
## Utilidades de Bootstrap para Responsive
### Display
```html
<!-- Ocultar en mobile, mostrar en desktop -->
<div class="d-none d-lg-block">Solo en desktop</div>
<!-- Mostrar en mobile, ocultar en desktop -->
<div class="d-lg-none">Solo en mobile</div>
<!-- Mostrar en todos los tamaños -->
<div class="d-block">Siempre visible</div>
```
### Flex Direction
```html
<!-- Columna en mobile, fila en desktop -->
<div class="d-flex flex-column flex-lg-row">
<div>Item 1</div>
<div>Item 2</div>
</div>
```
### Text Alignment
```html
<!-- Centrado en mobile, izquierda en desktop -->
<p class="text-center text-lg-start">Texto</p>
```
---
## Testing Responsive
### Checklist
- [ ] Probar en mobile (<576px)
- [ ] Probar en tablet (768px)
- [ ] Probar en desktop (≥992px)
- [ ] Header del tab se adapta correctamente
- [ ] Grid de 2 columnas se convierte en stack vertical
- [ ] Cards mantienen legibilidad
- [ ] Botones no se cortan
- [ ] Vista previa funciona en todos los tamaños
### Herramientas
1. **Chrome DevTools**: F12 → Toggle Device Toolbar (Ctrl+Shift+M)
2. **Firefox Responsive Design Mode**: F12 → Toggle Responsive Design Mode
3. **Dispositivos reales**: Probar en iPhone, iPad, Android
---
## Volver al Índice
[← Volver al README](README.md)

View File

@@ -0,0 +1,548 @@
# 💻 JAVASCRIPT PATTERNS
## 1. Inicialización del Componente
```javascript
document.addEventListener('DOMContentLoaded', function() {
console.log('✅ [ComponentName] Admin Panel cargado');
// 1. Cargar configuración guardada
loadConfig();
// 2. Inicializar vista previa
updatePreview();
// 3. Conectar event listeners
initializeEventListeners();
});
```
**Orden de ejecución:**
1. `loadConfig()`: Carga valores guardados desde localStorage/JSON
2. `updatePreview()`: Renderiza el preview inicial
3. `initializeEventListeners()`: Conecta los campos al preview
---
## 2. Event Listeners
### Patrón Básico
```javascript
function initializeEventListeners() {
// Lista de campos que deben actualizar el preview
const fields = [
'enabled',
'showOnMobile',
'bgColor',
'textColor',
'highlightText',
'messageText',
'showLink',
'linkText',
'linkUrl'
];
// Conectar event listeners automáticamente
fields.forEach(fieldId => {
const element = document.getElementById(fieldId);
if (element) {
// Checkboxes: usar 'change'
if (element.type === 'checkbox') {
element.addEventListener('change', updatePreview);
}
// Otros inputs: usar 'input' para tiempo real
else {
element.addEventListener('input', updatePreview);
}
}
});
// Event listeners específicos
initializeColorPickerListeners();
initializeTextareaCounters();
initializeResetButton();
}
```
### Color Pickers con Display Hex
```javascript
function initializeColorPickerListeners() {
const colorFields = [
{ input: 'bgColor', display: 'bgColorValue' },
{ input: 'textColor', display: 'textColorValue' },
{ input: 'highlightColor', display: 'highlightColorValue' }
];
colorFields.forEach(({ input, display }) => {
const inputElement = document.getElementById(input);
const displayElement = document.getElementById(display);
if (inputElement && displayElement) {
inputElement.addEventListener('input', function() {
displayElement.textContent = this.value.toUpperCase();
updatePreview();
});
}
});
}
```
### Textareas con Contadores
```javascript
function initializeTextareaCounters() {
const textareas = [
{ field: 'messageText', counter: 'messageTextCount', progress: 'messageTextProgress', max: 250 },
{ field: 'description', counter: 'descriptionCount', progress: 'descriptionProgress', max: 500 }
];
textareas.forEach(({ field, counter, progress, max }) => {
const textarea = document.getElementById(field);
const counterElement = document.getElementById(counter);
const progressElement = document.getElementById(progress);
if (textarea && counterElement && progressElement) {
textarea.addEventListener('input', function() {
const length = this.value.length;
const percentage = (length / max) * 100;
// Actualizar contador
counterElement.textContent = length;
// Actualizar progress bar
progressElement.style.width = percentage + '%';
progressElement.setAttribute('aria-valuenow', length);
// Cambiar color según uso
if (percentage > 90) {
progressElement.style.backgroundColor = '#dc3545'; // Rojo
} else if (percentage > 75) {
progressElement.style.backgroundColor = '#ffc107'; // Amarillo
} else {
progressElement.style.backgroundColor = '#FF8600'; // Orange
}
updatePreview();
});
}
});
}
```
### Botón de Reset
```javascript
function initializeResetButton() {
const resetBtn = document.getElementById('resetDefaults');
if (resetBtn) {
resetBtn.addEventListener('click', resetToDefaults);
}
}
```
---
## 3. Función updatePreview()
```javascript
/**
* Actualiza la vista previa en tiempo real
* REGLA: Solo modificar propiedades que el usuario puede cambiar
*/
function updatePreview() {
const preview = document.getElementById('componentPreview');
if (!preview) return;
// 1. Activar/desactivar componente
const enabled = document.getElementById('enabled').checked;
preview.style.display = enabled ? 'block' : 'none';
// 2. Colores
const bgColor = document.getElementById('bgColor').value;
const textColor = document.getElementById('textColor').value;
preview.style.backgroundColor = bgColor;
preview.style.color = textColor;
// 3. Textos
const highlightText = document.getElementById('highlightText').value;
const messageText = document.getElementById('messageText').value;
const highlightElement = document.getElementById('previewHighlight');
const messageElement = document.getElementById('previewMessage');
if (highlightElement) highlightElement.textContent = highlightText;
if (messageElement) messageElement.textContent = messageText;
// 4. Mostrar/ocultar elementos
const showIcon = document.getElementById('showIcon').checked;
const iconElement = document.getElementById('previewIcon');
if (iconElement) {
iconElement.style.display = showIcon ? 'inline-block' : 'none';
}
// 5. Cambiar clase del icono
const iconClass = document.getElementById('iconClass').value;
if (iconElement && iconClass) {
iconElement.className = iconClass + ' notification-icon';
}
// 6. Link
const showLink = document.getElementById('showLink').checked;
const linkElement = document.getElementById('previewLink');
if (linkElement) {
linkElement.style.display = showLink ? 'inline-block' : 'none';
if (showLink) {
const linkText = document.getElementById('linkText').value;
const linkUrl = document.getElementById('linkUrl').value;
const linkTarget = document.getElementById('linkTarget').value;
linkElement.textContent = linkText;
linkElement.href = linkUrl;
linkElement.target = linkTarget;
}
}
}
```
---
## 4. Guardar Configuración
### localStorage (Temporal)
```javascript
/**
* Guarda la configuración en localStorage
*/
function saveConfig() {
const config = {
enabled: document.getElementById('enabled').checked,
showOnMobile: document.getElementById('showOnMobile').checked,
showOnDesktop: document.getElementById('showOnDesktop').checked,
bgColor: document.getElementById('bgColor').value,
textColor: document.getElementById('textColor').value,
highlightColor: document.getElementById('highlightColor').value,
highlightText: document.getElementById('highlightText').value,
messageText: document.getElementById('messageText').value,
showIcon: document.getElementById('showIcon').checked,
iconClass: document.getElementById('iconClass').value,
showLink: document.getElementById('showLink').checked,
linkText: document.getElementById('linkText').value,
linkUrl: document.getElementById('linkUrl').value,
linkTarget: document.getElementById('linkTarget').value
};
localStorage.setItem('componentConfig', JSON.stringify(config));
console.log('💾 Configuración guardada:', config);
}
```
### Archivo JSON (Persistente)
```javascript
/**
* Guarda la configuración en archivo JSON
*/
async function saveConfigToFile() {
const config = {
component: '[component-name]', // Nombre del componente en kebab-case
version: '1.0',
lastModified: new Date().toISOString(),
config: {
enabled: document.getElementById('enabled').checked,
bgColor: document.getElementById('bgColor').value,
textColor: document.getElementById('textColor').value,
messageText: document.getElementById('messageText').value,
// ... todos los campos del componente
}
};
try {
const response = await fetch('./config.json', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(config, null, 2)
});
if (response.ok) {
console.log('💾 Configuración guardada exitosamente');
showNotification('Cambios guardados', 'success');
} else {
throw new Error('Error al guardar');
}
} catch (error) {
console.error('❌ Error al guardar:', error);
showNotification('Error al guardar cambios', 'error');
}
}
```
---
## 5. Cargar Configuración
### Desde localStorage
```javascript
/**
* Carga la configuración desde localStorage
*/
function loadConfig() {
const saved = localStorage.getItem('componentConfig');
if (!saved) {
console.log(' No hay configuración guardada, usando valores por defecto');
return;
}
const config = JSON.parse(saved);
// Aplicar valores guardados
Object.keys(config).forEach(key => {
const element = document.getElementById(key);
if (element) {
if (element.type === 'checkbox') {
element.checked = config[key];
} else if (element.type === 'color') {
element.value = config[key];
// Actualizar display del hex
const displayElement = document.getElementById(key + 'Value');
if (displayElement) {
displayElement.textContent = config[key].toUpperCase();
}
} else {
element.value = config[key];
}
}
});
console.log('📂 Configuración cargada:', config);
updatePreview();
}
```
### Desde archivo JSON
```javascript
/**
* Carga la configuración desde archivo JSON
*/
async function loadConfigFromFile() {
try {
const response = await fetch('./config.json');
if (!response.ok) {
throw new Error('Config file not found');
}
const data = await response.json();
const config = data.config;
// Aplicar valores cargados (igual que con localStorage)
Object.keys(config).forEach(key => {
const element = document.getElementById(key);
if (element) {
if (element.type === 'checkbox') {
element.checked = config[key];
} else {
element.value = config[key];
}
}
});
console.log('📂 Configuración cargada desde archivo:', config);
updatePreview();
} catch (error) {
console.log(' No se encontró config.json, usando valores por defecto');
resetToDefaults();
}
}
```
---
## 6. Reset a Valores por Defecto
```javascript
/**
* Restaura los valores por defecto
*/
function resetToDefaults() {
if (!confirm('¿Estás seguro de restaurar los valores por defecto?')) {
return;
}
// Valores por defecto
const defaults = {
enabled: true,
showOnMobile: true,
showOnDesktop: true,
bgColor: '#0E2337',
textColor: '#ffffff',
highlightColor: '#FF8600',
highlightText: 'Nuevo:',
messageText: 'Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.',
showIcon: true,
iconClass: 'bi bi-megaphone-fill',
showLink: true,
linkText: 'Ver Catálogo →',
linkUrl: '/catalogo',
linkTarget: '_self',
fontSize: 'normal'
};
// Aplicar defaults
Object.keys(defaults).forEach(key => {
const element = document.getElementById(key);
if (element) {
if (element.type === 'checkbox') {
element.checked = defaults[key];
} else {
element.value = defaults[key];
}
// Actualizar display de colores
if (element.type === 'color') {
const displayElement = document.getElementById(key + 'Value');
if (displayElement) {
displayElement.textContent = defaults[key].toUpperCase();
}
}
}
});
updatePreview();
saveConfig();
console.log('🔄 Valores por defecto restaurados');
}
```
---
## 7. Validación de Formularios
```javascript
/**
* Valida los campos del formulario
*/
function validateForm() {
let isValid = true;
const errors = [];
// 1. Validar campo requerido
const messageText = document.getElementById('messageText').value.trim();
if (!messageText) {
errors.push('El mensaje principal es requerido');
isValid = false;
}
// 2. Validar longitud
if (messageText.length > 250) {
errors.push('El mensaje no puede exceder 250 caracteres');
isValid = false;
}
// 3. Validar URL
const linkUrl = document.getElementById('linkUrl').value.trim();
if (linkUrl && !isValidUrl(linkUrl)) {
errors.push('La URL no es válida');
isValid = false;
}
// 4. Validar formato de clase CSS
const iconClass = document.getElementById('iconClass').value.trim();
if (iconClass && !/^[\w\s-]+$/.test(iconClass)) {
errors.push('La clase del icono contiene caracteres inválidos');
isValid = false;
}
if (!isValid) {
alert('⚠️ Errores de validación:\n\n' + errors.join('\n'));
}
return isValid;
}
/**
* Valida si una URL es válida
*/
function isValidUrl(string) {
// Permitir rutas relativas
if (string.startsWith('/')) {
return true;
}
// Validar URLs absolutas
try {
new URL(string);
return true;
} catch (_) {
return false;
}
}
```
---
## 8. Sistema de Notificaciones
```javascript
/**
* Muestra notificación temporal
*/
function showNotification(message, type = 'success') {
const notification = document.createElement('div');
notification.className = `alert alert-${type === 'success' ? 'success' : 'danger'} position-fixed top-0 start-50 translate-middle-x mt-3`;
notification.style.zIndex = '9999';
notification.innerHTML = `
<i class="bi bi-${type === 'success' ? 'check-circle' : 'exclamation-circle'} me-2"></i>
${message}
`;
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
}
// Uso
showNotification('Cambios guardados', 'success');
showNotification('Error al guardar cambios', 'error');
```
---
## 9. Auto-save (Opcional)
```javascript
/**
* Auto-guarda cada vez que cambia un campo
*/
function initializeAutoSave() {
const fields = document.querySelectorAll('input, select, textarea');
fields.forEach(field => {
field.addEventListener('change', function() {
saveConfig();
console.log('💾 Auto-guardado');
});
});
}
// Llamar después de initializeEventListeners()
```
---
## Volver al Índice
[← Volver al README](README.md)

View File

@@ -0,0 +1,443 @@
# 💾 PERSISTENCIA EN ARCHIVOS JSON
## Estructura del Archivo config.json
Cada componente DEBE tener su archivo `config.json` en su carpeta:
```json
{
"component": "[component-name]",
"version": "1.0",
"lastModified": "2025-01-15T10:30:00Z",
"config": {
"enabled": true,
"showOnMobile": true,
"showOnDesktop": true,
"bgColor": "#0E2337",
"textColor": "#ffffff",
"highlightColor": "#FF8600",
"linkHoverColor": "#FF6B35",
"fontSize": "normal",
"showIcon": true,
"iconClass": "bi bi-icon-name",
"highlightText": "Texto destacado",
"messageText": "Mensaje principal del componente",
"showLink": true,
"linkText": "Texto del enlace",
"linkUrl": "/ruta",
"linkTarget": "_self"
}
}
```
### Campos del Metadata
| Campo | Tipo | Descripción |
|-------|------|-------------|
| `component` | string | Nombre del componente (kebab-case) |
| `version` | string | Versión del config (semver) |
| `lastModified` | string | Timestamp ISO 8601 de última modificación |
| `config` | object | Objeto con la configuración del componente |
---
## Funciones de Persistencia
### Guardar Configuración
```javascript
/**
* Guarda la configuración en archivo JSON
*/
async function saveConfig() {
const config = {
component: '[component-name]', // Nombre del componente en kebab-case
version: '1.0',
lastModified: new Date().toISOString(),
config: {
enabled: document.getElementById('enabled').checked,
showOnMobile: document.getElementById('showOnMobile').checked,
showOnDesktop: document.getElementById('showOnDesktop').checked,
bgColor: document.getElementById('bgColor').value,
textColor: document.getElementById('textColor').value,
highlightColor: document.getElementById('highlightColor').value,
linkHoverColor: document.getElementById('linkHoverColor').value,
fontSize: document.getElementById('fontSize').value,
showIcon: document.getElementById('showIcon').checked,
iconClass: document.getElementById('iconClass').value,
highlightText: document.getElementById('highlightText').value,
messageText: document.getElementById('messageText').value,
showLink: document.getElementById('showLink').checked,
linkText: document.getElementById('linkText').value,
linkUrl: document.getElementById('linkUrl').value,
linkTarget: document.getElementById('linkTarget').value
}
};
try {
// Guardar en archivo JSON
const response = await fetch('./config.json', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(config, null, 2)
});
if (response.ok) {
console.log('💾 Configuración guardada exitosamente');
showNotification('Cambios guardados', 'success');
} else {
throw new Error('Error al guardar');
}
} catch (error) {
console.error('❌ Error al guardar:', error);
showNotification('Error al guardar cambios', 'error');
}
}
```
---
### Cargar Configuración
```javascript
/**
* Carga la configuración desde archivo JSON
*/
async function loadConfig() {
try {
const response = await fetch('./config.json');
if (!response.ok) {
throw new Error('Config file not found');
}
const data = await response.json();
const config = data.config;
// Validar estructura
if (!validateConfigStructure(data)) {
throw new Error('Invalid config structure');
}
// Aplicar valores cargados
if (config.enabled !== undefined) {
document.getElementById('enabled').checked = config.enabled;
}
if (config.showOnMobile !== undefined) {
document.getElementById('showOnMobile').checked = config.showOnMobile;
}
if (config.showOnDesktop !== undefined) {
document.getElementById('showOnDesktop').checked = config.showOnDesktop;
}
if (config.bgColor) {
document.getElementById('bgColor').value = config.bgColor;
document.getElementById('bgColorValue').textContent = config.bgColor.toUpperCase();
}
if (config.textColor) {
document.getElementById('textColor').value = config.textColor;
document.getElementById('textColorValue').textContent = config.textColor.toUpperCase();
}
if (config.highlightColor) {
document.getElementById('highlightColor').value = config.highlightColor;
document.getElementById('highlightColorValue').textContent = config.highlightColor.toUpperCase();
}
if (config.fontSize) {
document.getElementById('fontSize').value = config.fontSize;
}
if (config.showIcon !== undefined) {
document.getElementById('showIcon').checked = config.showIcon;
}
if (config.iconClass) {
document.getElementById('iconClass').value = config.iconClass;
}
if (config.highlightText) {
document.getElementById('highlightText').value = config.highlightText;
}
if (config.messageText) {
document.getElementById('messageText').value = config.messageText;
// Actualizar contador si existe
const counter = document.getElementById('messageTextCount');
if (counter) {
counter.textContent = config.messageText.length;
}
}
if (config.showLink !== undefined) {
document.getElementById('showLink').checked = config.showLink;
}
if (config.linkText) {
document.getElementById('linkText').value = config.linkText;
}
if (config.linkUrl) {
document.getElementById('linkUrl').value = config.linkUrl;
}
if (config.linkTarget) {
document.getElementById('linkTarget').value = config.linkTarget;
}
console.log('📂 Configuración cargada:', config);
updatePreview();
} catch (error) {
console.log(' No se encontró config.json, usando valores por defecto');
resetToDefaults();
}
}
```
---
## Validación de JSON
```javascript
/**
* Valida la estructura del archivo config.json
*/
function validateConfigStructure(data) {
const required = ['component', 'version', 'config'];
// Verificar campos requeridos
for (const field of required) {
if (!data.hasOwnProperty(field)) {
console.error(`❌ Campo requerido faltante: ${field}`);
return false;
}
}
// Verificar que config sea un objeto
if (typeof data.config !== 'object' || data.config === null) {
console.error('❌ El campo "config" debe ser un objeto');
return false;
}
return true;
}
```
---
## Sistema de Notificaciones
```javascript
/**
* Muestra notificación temporal
*/
function showNotification(message, type = 'success') {
const notification = document.createElement('div');
notification.className = `alert alert-${type === 'success' ? 'success' : 'danger'} position-fixed top-0 start-50 translate-middle-x mt-3`;
notification.style.zIndex = '9999';
notification.innerHTML = `
<i class="bi bi-${type === 'success' ? 'check-circle' : 'exclamation-circle'} me-2"></i>
${message}
`;
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
}
// Uso
showNotification('Cambios guardados', 'success');
showNotification('Error al guardar cambios', 'error');
showNotification('Configuración restaurada', 'success');
```
---
## Migración de localStorage a JSON
Si tienes datos en localStorage que quieres migrar a JSON:
```javascript
/**
* Migra la configuración de localStorage a archivo JSON
*/
async function migrateFromLocalStorage() {
const saved = localStorage.getItem('topBarConfig');
if (!saved) {
console.log(' No hay datos en localStorage para migrar');
return;
}
const config = JSON.parse(saved);
// Crear estructura de config.json
const jsonConfig = {
component: '[component-name]', // Nombre del componente en kebab-case
version: '1.0',
lastModified: new Date().toISOString(),
config: config
};
try {
const response = await fetch('./config.json', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(jsonConfig, null, 2)
});
if (response.ok) {
console.log('✅ Migración exitosa de localStorage a JSON');
// Opcional: limpiar localStorage
localStorage.removeItem('topBarConfig');
}
} catch (error) {
console.error('❌ Error al migrar:', error);
}
}
```
---
## Manejo de Errores
```javascript
/**
* Guarda con manejo completo de errores
*/
async function saveConfigWithErrorHandling() {
// 1. Validar antes de guardar
if (!validateForm()) {
return;
}
// 2. Preparar datos
const config = buildConfigObject();
// 3. Intentar guardar
try {
const response = await fetch('./config.json', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(config, null, 2)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
console.log('💾 Configuración guardada exitosamente');
showNotification('Cambios guardados correctamente', 'success');
} catch (error) {
console.error('❌ Error al guardar:', error);
// Notificar al usuario
showNotification('Error al guardar. Verifica la conexión.', 'error');
// Fallback: guardar en localStorage temporalmente
localStorage.setItem('topBarConfig_backup', JSON.stringify(config));
console.log('💾 Backup guardado en localStorage');
}
}
/**
* Construye el objeto de configuración
*/
function buildConfigObject() {
return {
component: '[component-name]', // Nombre del componente en kebab-case
version: '1.0',
lastModified: new Date().toISOString(),
config: {
enabled: document.getElementById('enabled').checked,
bgColor: document.getElementById('bgColor').value,
// ... resto de campos
}
};
}
```
---
## Backup y Restore
### Crear Backup
```javascript
/**
* Crea un backup de la configuración actual
*/
async function createBackup() {
try {
const response = await fetch('./config.json');
const config = await response.json();
// Agregar timestamp al nombre del backup
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupName = `config_backup_${timestamp}.json`;
// Descargar como archivo
const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = backupName;
a.click();
URL.revokeObjectURL(url);
showNotification('Backup creado exitosamente', 'success');
} catch (error) {
console.error('❌ Error al crear backup:', error);
showNotification('Error al crear backup', 'error');
}
}
```
### Restaurar Backup
```javascript
/**
* Restaura un backup
*/
function restoreBackup(file) {
const reader = new FileReader();
reader.onload = async function(e) {
try {
const config = JSON.parse(e.target.result);
// Validar estructura
if (!validateConfigStructure(config)) {
throw new Error('Invalid backup structure');
}
// Aplicar configuración
const data = config.config;
Object.keys(data).forEach(key => {
const element = document.getElementById(key);
if (element) {
if (element.type === 'checkbox') {
element.checked = data[key];
} else {
element.value = data[key];
}
}
});
updatePreview();
showNotification('Backup restaurado exitosamente', 'success');
} catch (error) {
console.error('❌ Error al restaurar backup:', error);
showNotification('Error al restaurar backup', 'error');
}
};
reader.readAsText(file);
}
```
---
## Volver al Índice
[← Volver al README](README.md)

View File

@@ -0,0 +1,473 @@
# 🎛️ PANEL DE ADMINISTRACIÓN PRINCIPAL
## Arquitectura del Sistema
El panel de administración utiliza un **sistema de tabs (pestañas)** de Bootstrap 5, donde todos los componentes existen en una sola página y se alternan mediante JavaScript.
### Características Principales
-**Single Page**: Todos los componentes en un solo archivo `main.php`
-**Bootstrap Tabs**: Navegación mediante `nav-tabs` y `tab-pane`
-**Carga Modular**: Cada componente se incluye con `require_once`
-**Botones Globales**: Save/Cancel compartidos para todos los componentes
-**Sin Recargas**: Cambio de tabs sin reload de página
---
## Estructura del main.php
```php
<?php
/**
* Admin Panel - Main Page
*/
if (!defined('ABSPATH')) {
exit;
}
?>
<div class="wrap apus-admin">
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
<p class="description">Configure los componentes del tema</p>
<!-- Navigation Tabs -->
<ul class="nav nav-tabs" role="tablist">
<!-- Tabs aquí -->
</ul>
<!-- Tab Content -->
<div class="tab-content mt-3">
<?php
// Componentes incluidos aquí
?>
</div>
<!-- Action Buttons (Global) -->
<div class="d-flex justify-content-end gap-2 p-3 rounded border mt-4"
style="background-color: #f8f9fa;">
<button type="button" class="btn btn-outline-secondary" id="cancelChanges">
<i class="bi bi-x-circle me-1"></i>
Cancelar
</button>
<button type="button" id="saveSettings"
class="btn fw-semibold text-white"
style="background-color: #FF8600; border-color: #FF8600;">
<i class="bi bi-check-circle me-1"></i>
Guardar Cambios
</button>
</div>
</div>
```
---
## Agregar un Nuevo Tab
### Paso 1: Agregar el Tab en la Navegación
```php
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active"
data-bs-toggle="tab"
data-bs-target="#[tabId]"
href="#[tabId]"
role="tab">
<i class="bi bi-[icon-class] me-2"></i>
[Nombre del Componente]
</a>
</li>
<!-- Más tabs... -->
</ul>
```
**Reemplazar:**
- `[tabId]`: ID único del tab (ej: `notificationBarTab`, `siteFooterTab`)
- `[icon-class]`: Clase del icono de Bootstrap Icons (ej: `megaphone-fill`)
- `[Nombre del Componente]`: Nombre visible del tab (ej: "Barra de Notificaciones")
### Paso 2: Incluir el Componente en Tab Content
```php
<div class="tab-content mt-3">
<?php
/**
* [Component Name] Component
* Archivo: Admin/Components/component-[name].php
*/
require_once APUS_ADMIN_PANEL_PATH . 'Admin/Components/component-[name].php';
?>
<!-- Más componentes... -->
</div>
```
**Reemplazar:**
- `[Component Name]`: Nombre descriptivo del componente
- `[name]`: Nombre del archivo (ej: `notification-bar`, `site-footer`)
### Paso 3: Crear el Archivo del Componente
Crear `Admin/Components/component-[name].php`:
```php
<!-- Tab Pane: [Component Name] -->
<div class="tab-pane fade show active" id="[tabId]" role="tabpanel">
<!-- Header del Tab -->
<div class="rounded p-4 mb-4 shadow text-white"
style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%);
border-left: 4px solid #FF8600;">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
<div>
<h3 class="h4 mb-1 fw-bold">
<i class="bi bi-[icon-class] me-2" style="color: #FF8600;"></i>
[Título del Componente]
</h3>
<p class="mb-0 small" style="opacity: 0.85;">
[Descripción breve del componente]
</p>
</div>
<button type="button" class="btn btn-sm btn-outline-light" id="reset[Component]Defaults">
<i class="bi bi-arrow-counterclockwise me-1"></i>
Restaurar valores por defecto
</button>
</div>
</div>
<!-- Grid de Configuración -->
<div class="row g-3">
<!-- Columna izquierda: Configuración -->
<div class="col-lg-6">
<!-- Cards de configuración -->
</div>
<!-- Columna derecha: Preview -->
<div class="col-lg-6">
<!-- Card de vista previa -->
</div>
</div>
</div><!-- /tab-pane -->
```
---
## Iconos Sugeridos por Tipo de Componente
| Tipo de Componente | Icono | Clase Bootstrap Icons |
|--------------------|-------|----------------------|
| Notificaciones/Anuncios | 📢 | `bi-megaphone-fill` |
| Navegación/Menú | 📋 | `bi-layout-text-window` |
| Sección Hero | 🖼️ | `bi-image` |
| Footer | ⬇️ | `bi-box-arrow-down` |
| Formularios | 📝 | `bi-clipboard-check` |
| Call-to-Action | 👆 | `bi-cursor-fill` |
| Testimonios | 💬 | `bi-chat-quote` |
| Configuración | ⚙️ | `bi-gear-fill` |
---
## Ejemplo Completo: Agregar Componente de Navegación
### 1. Agregar Tab en main.php
```php
<ul class="nav nav-tabs" role="tablist">
<!-- Tab existente -->
<li class="nav-item">
<a class="nav-link active"
data-bs-toggle="tab"
data-bs-target="#topBarTab"
href="#topBarTab"
role="tab">
<i class="bi bi-megaphone-fill me-2"></i>
Top Bar
</a>
</li>
<!-- NUEVO TAB -->
<li class="nav-item">
<a class="nav-link"
data-bs-toggle="tab"
data-bs-target="#navbarTab"
href="#navbarTab"
role="tab">
<i class="bi bi-layout-text-window me-2"></i>
Navbar
</a>
</li>
</ul>
```
**Notas:**
- El primer tab tiene `class="nav-link active"`
- Los demás tabs solo tienen `class="nav-link"`
### 2. Incluir Componente en Tab Content
```php
<div class="tab-content mt-3">
<?php
// Componente existente
require_once APUS_ADMIN_PANEL_PATH . 'Admin/Components/component-top-bar.php';
// NUEVO COMPONENTE
require_once APUS_ADMIN_PANEL_PATH . 'Admin/Components/component-navbar.php';
?>
</div>
```
### 3. Crear component-navbar.php
Crear archivo: `Admin/Components/component-navbar.php`
```php
<!-- Tab Pane: Navbar -->
<div class="tab-pane fade" id="navbarTab" role="tabpanel">
<!-- Header del Tab -->
<div class="rounded p-4 mb-4 shadow text-white"
style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%);
border-left: 4px solid #FF8600;">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
<div>
<h3 class="h4 mb-1 fw-bold">
<i class="bi bi-layout-text-window me-2" style="color: #FF8600;"></i>
Configuración del Navbar
</h3>
<p class="mb-0 small" style="opacity: 0.85;">
Gestiona los colores, estilo y comportamiento del menú de navegación
</p>
</div>
<button type="button" class="btn btn-sm btn-outline-light" id="resetNavbarDefaults">
<i class="bi bi-arrow-counterclockwise me-1"></i>
Restaurar valores por defecto
</button>
</div>
</div>
<!-- Grid de Configuración -->
<div class="row g-3">
<!-- Configuración -->
<div class="col-lg-6">
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
<div class="card-body">
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
<i class="bi bi-palette me-2" style="color: #FF8600;"></i>
Colores
</h5>
<!-- Campos de configuración aquí -->
</div>
</div>
</div>
<!-- Vista Previa -->
<div class="col-lg-6">
<div class="card shadow-sm mb-3" style="border-left: 4px solid #FF8600;">
<div class="card-body">
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
<i class="bi bi-eye me-2" style="color: #FF8600;"></i>
Vista Previa
</h5>
<!-- Preview del componente aquí -->
<div id="navbarPreview">
<!-- HTML idéntico al front-end -->
</div>
</div>
</div>
</div>
</div>
</div><!-- /tab-pane -->
```
**Notas:**
- El primer tab-pane tiene `class="tab-pane fade show active"`
- Los demás tab-panes tienen `class="tab-pane fade"`
---
## Orden de los Tabs
### Por Prioridad
Organizar los tabs de más importante a menos importante:
1. Componentes críticos/visibles (Header, Navbar, Hero)
2. Componentes de contenido (Secciones, CTAs)
3. Componentes complementarios (Footer, Forms)
4. Configuraciones generales
### Ejemplo de Orden
```php
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item"><!-- Barra de Notificaciones --></li>
<li class="nav-item"><!-- Navbar --></li>
<li class="nav-item"><!-- Hero Section --></li>
<li class="nav-item"><!-- Call-to-Action --></li>
<li class="nav-item"><!-- Footer --></li>
<li class="nav-item"><!-- Configuración General --></li>
</ul>
```
---
## Botones Globales Save/Cancel
Los botones de guardar y cancelar son **globales** para todos los tabs:
```php
<div class="d-flex justify-content-end gap-2 p-3 rounded border mt-4"
style="background-color: #f8f9fa; border-color: #e9ecef !important;">
<!-- Cancelar -->
<button type="button" class="btn btn-outline-secondary" id="cancelChanges">
<i class="bi bi-x-circle me-1"></i>
Cancelar
</button>
<!-- Guardar -->
<button type="button" id="saveSettings"
class="btn fw-semibold text-white"
style="background-color: #FF8600; border-color: #FF8600;"
disabled>
<i class="bi bi-check-circle me-1"></i>
Guardar Cambios
</button>
</div>
```
### JavaScript para Activar/Desactivar Guardar
```javascript
// Detectar cambios en cualquier campo
document.querySelectorAll('input, select, textarea').forEach(field => {
field.addEventListener('change', function() {
// Activar botón de guardar
document.getElementById('saveSettings').disabled = false;
});
});
// Guardar cambios
document.getElementById('saveSettings').addEventListener('click', function() {
// Guardar configuración de todos los componentes
saveAllConfigs();
// Desactivar botón de guardar
this.disabled = true;
});
// Cancelar cambios
document.getElementById('cancelChanges').addEventListener('click', function() {
if (confirm('¿Descartar todos los cambios sin guardar?')) {
// Recargar configuraciones originales
loadAllConfigs();
// Desactivar botón de guardar
document.getElementById('saveSettings').disabled = true;
}
});
```
---
## Responsive Behavior
### Desktop (≥992px)
- Tabs en una sola línea horizontal
- Grid de 2 columnas (configuración + preview)
- Espaciado completo
### Tablet (768px - 991px)
- Tabs pueden hacer wrap a 2 líneas
- Grid de 2 columnas (se mantiene)
- Espaciado reducido
### Mobile (<768px)
- Tabs en scroll horizontal o stacked vertical
- Grid stacked (1 columna)
- Botones Save/Cancel pueden hacer stack
```css
@media (max-width: 991px) {
.nav-tabs {
flex-wrap: wrap;
}
.nav-tabs .nav-link {
font-size: 0.9rem;
padding: 0.5rem 0.75rem;
}
}
@media (max-width: 767px) {
.nav-tabs {
overflow-x: auto;
flex-wrap: nowrap;
}
.nav-tabs .nav-item {
white-space: nowrap;
}
}
```
---
## Funcionalidad Adicional (Opcional)
### Badges de Estado en Tabs
```php
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" data-bs-target="#[tabId]" role="tab">
<i class="bi bi-[icon-class] me-2"></i>
[Nombre del Componente]
<span class="badge bg-success ms-2" style="font-size: 0.65rem;">Activo</span>
</a>
</li>
```
**Estados posibles:**
- `bg-success`: Activo
- `bg-secondary`: Inactivo
- `bg-warning text-dark`: Requiere atención
### Indicador de Cambios Sin Guardar
```javascript
// Agregar asterisco al tab si tiene cambios sin guardar
function markTabAsModified(tabId) {
const tabLink = document.querySelector(`a[data-bs-target="#${tabId}"]`);
if (!tabLink.querySelector('.modified-indicator')) {
tabLink.innerHTML += ' <span class="modified-indicator" style="color: #FF8600;">*</span>';
}
}
```
---
## Checklist de Implementación
Cuando agregues un nuevo componente al panel, asegúrate de:
- [ ] Agregar el tab en la navegación (`<ul class="nav nav-tabs">`)
- [ ] Crear el archivo PHP del componente (`component-[name].php`)
- [ ] Incluir el componente en tab-content con `require_once`
- [ ] Crear archivo CSS del componente (`component-[name].css`)
- [ ] Crear archivo JS del componente (`component-[name].js`)
- [ ] Crear archivo de configuración JSON (`[name]-config.json`)
- [ ] Agregar estilos específicos del componente
- [ ] Implementar vista previa en tiempo real
- [ ] Conectar con sistema de guardar/cancelar global
- [ ] Probar responsive behavior
- [ ] Verificar que el tab-pane tenga el ID correcto
- [ ] Verificar que el primer tab y tab-pane tengan `active` y `show`
---
## Volver al Índice
[← Volver al README](README.md)

View File

@@ -0,0 +1,395 @@
# ⚠️ CONFLICTOS CON WORDPRESS
## Conflicto 1: Bootstrap `.card` vs WordPress `.card`
### El Problema
WordPress Core tiene esta regla CSS que limita el ancho de las cards:
```css
/* WordPress Core CSS */
.card {
max-width: 520px;
}
```
Bootstrap 5 usa `.card` como componente principal:
```html
<div class="card">
<div class="card-body">...</div>
</div>
```
**CONFLICTO**: Cuando usas `<div class="card">` de Bootstrap dentro del admin de WordPress, WordPress aplica automáticamente `max-width: 520px`, limitando el ancho de tus cards.
---
### ❌ SOLUCIÓN INCORRECTA
**NO cambies `.card` por `.apus-card` o cualquier otra clase custom.**
**Razones:**
1. Bootstrap `.card` es un **sistema completo** con múltiples clases relacionadas:
- `.card-body`
- `.card-header`
- `.card-footer`
- `.card-title`
- `.card-text`
- `.card-img-top`
- Y muchas más...
2. Si cambias `.card` a `.apus-card`, pierdes **TODOS** los estilos de Bootstrap
3. Tendrías que recrear manualmente TODO el sistema de cards
---
### ✅ SOLUCIÓN CORRECTA
**Opción 1: Wrapper con Mayor Especificidad (RECOMENDADA)**
Agregar una clase wrapper en el `<body>` y sobreescribir el CSS de WordPress:
```html
<body class="apus-admin" style="font-family: 'Poppins', sans-serif; background-color: #f8f9fa;">
<div class="container-fluid py-4" style="max-width: 1400px;">
<!-- Todos tus cards de Bootstrap funcionan normalmente -->
<div class="card shadow-sm">
<div class="card-body">
<!-- Contenido -->
</div>
</div>
</div>
</body>
```
**CSS:**
```css
/* Sobreescribir el max-width de WordPress */
.apus-admin .card {
max-width: none !important;
}
/* Asegurar que funcione en todos los contextos */
body.apus-admin .card,
#apusAdminPanel .card {
max-width: none !important;
}
```
**Opción 2: Inline Style (Rápido)**
Si solo tienes unos pocos cards:
```html
<div class="card shadow-sm" style="max-width: none;">
<div class="card-body">
<!-- Contenido -->
</div>
</div>
```
**Opción 3: CSS Global**
```html
<style>
/* Sobreescribir WordPress max-width en todos los cards del admin */
body .card {
max-width: none !important;
}
</style>
```
---
## Conflicto 2: Form Switches (CRÍTICO)
### El Problema
Los switches de Bootstrap muestran **dos círculos** en lugar de un círculo que se desliza.
### Causa Raíz
1. **Pseudo-elemento `::before` de WordPress**: Muestra un SVG de checkmark (✓)
2. **`background-image` de Bootstrap**: Muestra el círculo del switch
3. **`background-size` incorrecto**: WordPress usa `auto` en lugar de `contain`
**Resultado**: Dos elementos visuales superpuestos en el switch.
---
### ✅ Solución Definitiva
```css
/* Eliminar completamente pseudo-elementos de WordPress */
body .form-switch .form-check-input[type="checkbox"]::before,
body .form-switch .form-check-input[type="checkbox"]::after,
.apus-admin .form-switch .form-check-input[type="checkbox"]::before,
.apus-admin .form-switch .form-check-input[type="checkbox"]::after {
content: none !important;
display: none !important;
background-image: none !important;
width: 0 !important;
height: 0 !important;
}
/* Configurar correctamente el background-size y repeat */
body .form-switch .form-check-input[type="checkbox"],
.apus-admin .form-switch .form-check-input[type="checkbox"] {
background-size: contain !important;
background-repeat: no-repeat !important;
background-position: left center !important;
}
body .form-switch .form-check-input[type="checkbox"]:checked,
.apus-admin .form-switch .form-check-input[type="checkbox"]:checked {
background-size: contain !important;
background-repeat: no-repeat !important;
background-position: right center !important;
}
```
**Qué hace cada parte:**
1. **Elimina `::before` y `::after`**: Anula completamente los pseudo-elementos de WordPress
2. **`content: none !important`**: Elimina cualquier contenido SVG de WordPress
3. **`display: none !important`**: Oculta el pseudo-elemento aunque tenga contenido
4. **`background-size: contain`**: Asegura que el círculo de Bootstrap se escale correctamente
5. **`background-repeat: no-repeat`**: Previene que el círculo se duplique
6. **`background-position`**: Controla la posición del círculo (left cuando OFF, right cuando ON)
---
## Conflicto 3: Alineación Vertical de Switches (CRÍTICO)
### El Problema
Los labels de los switches aparecen **desalineados verticalmente** con los switches, con una diferencia de aproximadamente **10.5px**.
### Causa Raíz
1. WordPress aplica `label { vertical-align: middle; }` globalmente
2. Bootstrap usa `display: block` + `float: left` en `.form-check`
3. Conflicto de layout entre elementos flotantes y `vertical-align`
---
### ✅ Solución Definitiva
```css
/* Fix alineación vertical de labels en switches */
#topBarTab .form-check.form-switch {
display: flex !important;
align-items: center !important;
}
#topBarTab .form-switch .form-check-input {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
#topBarTab .form-switch .form-check-label,
.form-switch .form-check-label.small {
line-height: 16px !important;
padding-top: 0 !important;
margin-top: 0 !important;
margin-bottom: 0 !important;
}
```
**Qué hace cada parte:**
1. **`display: flex`**: Convierte el contenedor en flexbox
2. **`align-items: center`**: Alinea todos los hijos verticalmente por su centro
3. **`line-height: 16px`**: Coincide con la altura del switch para alineación perfecta
4. **Márgenes en 0**: Elimina espacios verticales no deseados
**Resultado**: Diferencia de **0px** entre el centro del switch y el centro del label.
---
## Conflicto 4: `.button` (Raro)
WordPress también estiliza `.button`. **Solución**: Siempre usa `.btn` de Bootstrap:
```html
<!-- ✅ Seguro -->
<button class="btn btn-primary">OK</button>
<!-- ❌ Conflicto con WP -->
<button class="button">OK</button>
```
---
## Conflicto 5: `.row` (Raro)
Algunos temas de WordPress pueden estilizar `.row`. Si tienes problemas:
```css
.apus-admin .row {
/* Reset de cualquier estilo de WordPress */
display: flex;
flex-wrap: wrap;
margin-right: calc(var(--bs-gutter-x) * -0.5);
margin-left: calc(var(--bs-gutter-x) * -0.5);
}
```
---
## Conflicto 6: Colores de Links
WordPress usa colores específicos. Asegúrate de que tu CSS tenga mayor especificidad:
```css
/* WordPress puede tener reglas globales */
a { color: #0073aa; }
/* Tu CSS debe ser más específico */
.apus-admin a { color: #FF8600; }
.apus-admin .card a { color: #FF8600; }
```
---
## Template Actualizado con Todos los Fixes
```html
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin: [Component Name]</title>
<!-- Bootstrap 5.3.2 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/Css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/Font/bootstrap-icons.min.css">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../../Css/style.css">
<style>
body {
font-family: 'Poppins', sans-serif;
background-color: #f8f9fa;
}
/* FIX 1: Sobreescribir max-width de WordPress en Bootstrap cards */
body .card,
.apus-admin .card {
max-width: none !important;
}
/* FIX 2: Eliminar pseudo-elementos de WordPress en switches */
body .form-switch .form-check-input[type="checkbox"]::before,
body .form-switch .form-check-input[type="checkbox"]::after {
content: none !important;
display: none !important;
background-image: none !important;
width: 0 !important;
height: 0 !important;
}
/* FIX 3: Configurar background correctamente en switches */
body .form-switch .form-check-input[type="checkbox"] {
background-size: contain !important;
background-repeat: no-repeat !important;
background-position: left center !important;
}
body .form-switch .form-check-input[type="checkbox"]:checked {
background-position: right center !important;
}
/* FIX 4: Alineación vertical de labels en switches */
.form-check.form-switch {
display: flex !important;
align-items: center !important;
}
.form-switch .form-check-input {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.form-switch .form-check-label {
line-height: 16px !important;
padding-top: 0 !important;
margin-top: 0 !important;
margin-bottom: 0 !important;
}
/* Responsive */
@media (max-width: 991px) {
.tab-header { padding: 0.75rem; }
}
</style>
</head>
<body class="apus-admin">
<!-- Contenido del admin panel -->
</body>
</html>
```
---
## Checklist Anti-Conflictos WordPress
Antes de implementar un componente, verificar:
- [ ] Agregar clase `.apus-admin` al `<body>`
- [ ] **CRÍTICO**: Sobreescribir `.card { max-width: none !important; }`
- [ ] **CRÍTICO**: Eliminar pseudo-elementos `::before` y `::after` de switches
- [ ] **CRÍTICO**: Configurar `background-size: contain` en switches
- [ ] **CRÍTICO**: Configurar `background-repeat: no-repeat` en switches
- [ ] **CRÍTICO**: Configurar `background-position: left/right center` en switches
- [ ] **CRÍTICO**: Implementar `display: flex` + `align-items: center` en `.form-check.form-switch`
- [ ] **CRÍTICO**: Configurar `line-height: 16px` en labels de switches
- [ ] Usar `.btn` en lugar de `.button`
- [ ] Verificar que colores custom tengan `!important` si es necesario
- [ ] Usar DevTools para verificar que estilos de WordPress estén overridden
- [ ] **Probar switches**: deben mostrar UN solo círculo que se desliza
- [ ] **Probar alineación**: labels deben estar perfectamente alineados con switches (0px diferencia)
- [ ] Probar en un admin de WordPress real (si aplica)
---
## Verificación en DevTools
### Verificar Fix de Cards
```css
/* ✅ CORRECTO - Deberías ver: */
.apus-admin .card {
max-width: none !important; /* ✅ Tu CSS */
}
/* Y esto debería estar tachado/overridden: */
.card {
max-width: 520px; /* ❌ WordPress (overridden) */
}
```
### Verificar Fix de Switches
```css
/* ✅ CORRECTO - Deberías ver: */
.form-switch .form-check-input[type="checkbox"]::before {
content: none !important; /* ✅ Sin checkmark */
display: none !important; /* ✅ Oculto */
}
.form-switch .form-check-input[type="checkbox"] {
background-size: contain !important; /* ✅ Círculo escalado */
background-repeat: no-repeat !important; /* ✅ Sin duplicar */
}
```
---
## Volver al Índice
[← Volver al README](README.md)

View File

@@ -0,0 +1,420 @@
# 📦 EJEMPLOS COMPLETOS Y RECURSOS
## Ejemplo 1: Campo de Color con Valor Hex
```html
<div class="col-6">
<label for="primaryColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-paint-bucket me-1" style="color: #FF8600;"></i>
Color Primario
</label>
<input type="color" id="primaryColor"
class="form-control form-control-color w-100"
value="#0E2337"
title="Seleccionar color primario">
<small class="text-muted d-block mt-1" id="primaryColorValue">#0E2337</small>
</div>
<script>
const colorInput = document.getElementById('primaryColor');
const colorValue = document.getElementById('primaryColorValue');
colorInput.addEventListener('input', function() {
colorValue.textContent = this.value.toUpperCase();
updatePreview();
});
</script>
```
---
## Ejemplo 2: Campo de Texto con Icono de Bootstrap
```html
<div class="mb-2">
<label for="iconClass" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-emoji-smile me-1" style="color: #FF8600;"></i>
Clase del icono
<span class="badge bg-secondary" style="font-size: 0.65rem;">Bootstrap Icons</span>
</label>
<input type="text" id="iconClass"
class="form-control form-control-sm"
placeholder="bi bi-star-fill"
value="bi bi-megaphone-fill"
maxlength="50">
<small class="text-muted d-block mt-1">
<i class="bi bi-info-circle me-1"></i>
Ver: <a href="https://icons.getbootstrap.com/" target="_blank"
class="text-decoration-none" style="color: #FF8600;">
Bootstrap Icons <i class="bi bi-box-arrow-up-right"></i>
</a>
</small>
</div>
```
---
## Ejemplo 3: Textarea con Contador y Progress Bar
```html
<div class="mb-2">
<label for="description" class="form-label small mb-1 fw-semibold" style="color: #495057;">
<i class="bi bi-chat-left-text me-1" style="color: #FF8600;"></i>
Descripción <span class="text-danger">*</span>
<span class="float-end text-muted">
<span id="descriptionCount" class="fw-bold">0</span>/250
</span>
</label>
<textarea id="description"
class="form-control form-control-sm"
rows="3"
maxlength="250"
placeholder="Escribe una descripción atractiva..."
required></textarea>
<div class="progress mt-1" style="height: 3px;">
<div id="descriptionProgress"
class="progress-bar"
role="progressbar"
style="width: 0%; background-color: #FF8600;"
aria-valuenow="0"
aria-valuemin="0"
aria-valuemax="250"></div>
</div>
</div>
<script>
const textarea = document.getElementById('description');
const counter = document.getElementById('descriptionCount');
const progress = document.getElementById('descriptionProgress');
textarea.addEventListener('input', function() {
const length = this.value.length;
const maxLength = 250;
const percentage = (length / maxLength * 100);
counter.textContent = length;
progress.style.width = percentage + '%';
progress.setAttribute('aria-valuenow', length);
// Cambiar color según el uso
if (percentage > 90) {
progress.style.backgroundColor = '#dc3545'; // Rojo
} else if (percentage > 75) {
progress.style.backgroundColor = '#ffc107'; // Amarillo
} else {
progress.style.backgroundColor = '#FF8600'; // Orange
}
updatePreview();
});
</script>
```
---
## Template Básico Completo
```html
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin: [Component Name]</title>
<!-- Bootstrap 5.3.2 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/Css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/Font/bootstrap-icons.min.css">
<!-- Poppins Font -->
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
<!-- CSS del proyecto principal -->
<link rel="stylesheet" href="../../Css/style.css">
<style>
body {
font-family: 'Poppins', sans-serif;
background-color: #f8f9fa;
}
/* Fix WordPress .card max-width */
body .card {
max-width: none !important;
}
/* Fix WordPress switches */
body .form-switch .form-check-input[type="checkbox"]::before,
body .form-switch .form-check-input[type="checkbox"]::after {
content: none !important;
display: none !important;
}
body .form-switch .form-check-input[type="checkbox"] {
background-size: contain !important;
background-repeat: no-repeat !important;
background-position: left center !important;
}
body .form-switch .form-check-input[type="checkbox"]:checked {
background-position: right center !important;
}
/* Fix alineación vertical switches */
.form-check.form-switch {
display: flex !important;
align-items: center !important;
}
.form-switch .form-check-label {
line-height: 16px !important;
margin-top: 0 !important;
}
/* Responsive */
@media (max-width: 991px) {
.tab-header { padding: 0.75rem; }
}
</style>
</head>
<body>
<div class="container-fluid py-4" style="max-width: 1400px;">
<!-- Header del Tab -->
<div class="rounded p-4 mb-4 shadow text-white"
style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%);
border-left: 4px solid #FF8600;">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
<div>
<h3 class="h4 mb-1 fw-bold">
<i class="bi bi-[icon] me-2" style="color: #FF8600;"></i>
Configuración de [Component]
</h3>
<p class="mb-0 small" style="opacity: 0.85;">
[Descripción del componente]
</p>
</div>
<button type="button" class="btn btn-sm btn-outline-light" id="resetDefaults">
<i class="bi bi-arrow-counterclockwise me-1"></i>
Restaurar valores por defecto
</button>
</div>
</div>
<!-- Grid de Contenido -->
<div class="row g-3">
<!-- Columna Izquierda -->
<div class="col-lg-6">
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
<div class="card-body">
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
<i class="bi bi-palette me-2" style="color: #FF8600;"></i>
Sección 1
</h5>
<!-- Campos -->
</div>
</div>
</div>
<!-- Columna Derecha -->
<div class="col-lg-6">
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
<div class="card-body">
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
<i class="bi bi-gear me-2" style="color: #FF8600;"></i>
Sección 2
</h5>
<!-- Campos -->
</div>
</div>
</div>
<!-- Vista Previa -->
<div class="col-12">
<div class="card shadow-sm mb-3" style="border-left: 4px solid #FF8600;">
<div class="card-body">
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
<i class="bi bi-eye me-2" style="color: #FF8600;"></i>
Vista Previa en Tiempo Real
</h5>
<div id="componentPreview" class="[component-class]">
<!-- HTML idéntico al front-end -->
</div>
<div class="d-flex justify-content-between align-items-center mt-3">
<small class="text-muted">
<i class="bi bi-info-circle me-1"></i>
Los cambios se reflejan en tiempo real
</small>
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-secondary active" id="previewDesktop">
<i class="bi bi-display"></i> Desktop
</button>
<button type="button" class="btn btn-outline-secondary" id="previewMobile">
<i class="bi bi-phone"></i> Mobile
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/Js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
console.log('✅ Admin Panel cargado');
loadConfig();
updatePreview();
initializeEventListeners();
});
function initializeEventListeners() {
// Conectar event listeners
}
function updatePreview() {
// Actualizar vista previa
}
function saveConfig() {
// Guardar configuración
}
function loadConfig() {
// Cargar configuración
}
function resetToDefaults() {
if (!confirm('¿Estás seguro de restaurar los valores por defecto?')) {
return;
}
// Reset a defaults
}
</script>
</body>
</html>
```
---
## Checklist de Implementación
### Antes de Empezar
- [ ] Leer este manual completo
- [ ] Revisar la librería de componentes HTML (`../componentes-html-bootstrap/`)
- [ ] Identificar qué campos necesita el componente
- [ ] Determinar si necesita vista previa en tiempo real
### Estructura HTML
- [ ] Crear el header del tab con gradiente navy + border orange
- [ ] Incluir botón "Restaurar valores por defecto"
- [ ] Organizar campos en grid de 2 columnas (`col-lg-6`)
- [ ] Agrupar campos relacionados en cards con border-left navy
- [ ] Agregar card de vista previa con border-left orange
### Formularios
- [ ] Todos los labels tienen icono orange
- [ ] Labels importantes tienen `fw-semibold`
- [ ] Campos requeridos tienen `<span class="text-danger">*</span>`
- [ ] Color pickers muestran valor hex debajo
- [ ] Textareas tienen contador de caracteres + progress bar
- [ ] Selects usan `.form-select-sm`
- [ ] Inputs usan `.form-control-sm`
- [ ] Switches tienen icono + strong en label
### Vista Previa
- [ ] HTML de preview es IDÉNTICO al del front-end
- [ ] Se usa clase real del componente (ej: `.top-notification-bar`)
- [ ] NO hay inline styles que sobreescriban el CSS real
- [ ] Se cargan los mismos archivos CSS que el front-end
- [ ] Botones Desktop/Mobile funcionales
### JavaScript
- [ ] Función `updatePreview()` implementada
- [ ] Función `saveConfig()` guarda en localStorage/JSON
- [ ] Función `loadConfig()` carga al iniciar
- [ ] Función `resetToDefaults()` restaura valores
- [ ] Event listeners en todos los campos
- [ ] Color pickers actualizan el valor hex
- [ ] Textareas actualizan contador + progress bar
### CSS
- [ ] Fix WordPress `.card { max-width: none !important; }`
- [ ] Fix WordPress switches (pseudo-elementos)
- [ ] Fix alineación vertical switches
- [ ] Responsive breakpoints configurados
---
## Troubleshooting
### La vista previa no se ve igual al front-end
**Causa:** Inline styles sobreescribiendo el CSS real
**Solución:**
1. Verificar que NO haya inline styles en el HTML del preview
2. Verificar que se carga `../../Css/style.css`
3. Usar solo la clase del componente real
4. Si es necesario, agregar CSS con `!important` SOLO para el border
### Los colores no coinciden
**Causa:** No se están usando los valores correctos
**Solución:**
1. Usar valores hex exactos: `#0E2337`, `#FF8600`, etc.
2. Verificar que `style.css` define las variables `:root`
### El contador de caracteres no funciona
**Causa:** Event listener no conectado
**Solución:**
```javascript
textarea.addEventListener('input', function() {
counter.textContent = this.value.length;
});
```
### El responsive no funciona
**Causa:** Falta el viewport meta tag
**Solución:**
```html
<meta name="viewport" content="width=device-width, initial-scale=1.0">
```
---
## Recursos
### Bootstrap 5.3.2
- Documentación: https://getbootstrap.com/docs/5.3/
- Components: https://getbootstrap.com/docs/5.3/components/
### Bootstrap Icons
- Catálogo: https://icons.getbootstrap.com/
- Búsqueda: Usar el buscador para encontrar iconos
### Google Fonts - Poppins
- Pesos usados: 400 (Regular), 500 (Medium), 600 (Semi-Bold), 700 (Bold)
---
## Volver al Índice
[← Volver al README](README.md)

View File

@@ -0,0 +1,136 @@
# 🎨 ADMIN COMPONENTS - DESIGN SYSTEM (MODULARIZADO)
**Manual de Diseño para Componentes de Administración**
Versión 1.0 | APU México
---
## 📑 ÍNDICE DE DOCUMENTACIÓN
Este design system ha sido modularizado en archivos independientes para facilitar su consulta y mantenimiento.
### 📐 FUNDAMENTOS DEL SISTEMA
- [**01-ESPECIFICACIONES-DEL-SISTEMA.md**](01-ESPECIFICACIONES-DEL-SISTEMA.md)
- Requerimientos generales
- Stack tecnológico
- Estructura de archivos
- [**02-FILOSOFIA-DE-DISENO.md**](02-FILOSOFIA-DE-DISENO.md)
- Principios clave
- Características comunes
### 🎨 SISTEMA DE DISEÑO VISUAL
- [**03-PALETA-DE-COLORES.md**](03-PALETA-DE-COLORES.md)
- Colores principales (Navy, Orange, Neutral)
- Uso de colores por elemento
- [**04-TIPOGRAFIA.md**](04-TIPOGRAFIA.md)
- Font stack (Poppins)
- Pesos y tamaños
- Jerarquía visual
- [**05-SISTEMA-GRID-ESPACIADO.md**](05-SISTEMA-GRID-ESPACIADO.md)
- Grid de Bootstrap
- Breakpoints responsive
- Sistema de espaciado (margin/padding)
### 🏗️ ESTRUCTURA Y COMPONENTES
- [**06-ESTRUCTURA-LAYOUT.md**](06-ESTRUCTURA-LAYOUT.md)
- Container principal
- Header del tab (obligatorio)
- Sistema de grid 2 columnas
- [**07-COMPONENTES-REUTILIZABLES.md**](07-COMPONENTES-REUTILIZABLES.md)
- Botones (primario, secundario, reset)
- Inputs (text, color, textarea, select)
- Cards (estándar, vista previa)
- [**08-PATRONES-FORMULARIOS.md**](08-PATRONES-FORMULARIOS.md)
- Form switches
- Color pickers
- Text inputs con contador
- Badges y links de ayuda
### 🔄 FUNCIONALIDAD Y COMPORTAMIENTO
- [**09-VISTA-PREVIA-TIEMPO-REAL.md**](09-VISTA-PREVIA-TIEMPO-REAL.md)
- Estructura HTML del preview
- Reglas críticas
- CSS para vista previa
- [**10-RESPONSIVE-DESIGN.md**](10-RESPONSIVE-DESIGN.md)
- Breakpoints
- Patrones responsive
- Media queries
- [**11-JAVASCRIPT-PATTERNS.md**](11-JAVASCRIPT-PATTERNS.md)
- Inicialización
- Event listeners
- updatePreview()
- Guardar/cargar configuración
- [**12-PERSISTENCIA-JSON.md**](12-PERSISTENCIA-JSON.md)
- Estructura del config.json
- Funciones de persistencia
- Sistema de notificaciones
### 🎛️ PANEL Y ADMINISTRACIÓN
- [**13-PANEL-ADMINISTRACION.md**](13-PANEL-ADMINISTRACION.md)
- Estructura del panel principal
- Grid de componentes
- Patrón para nuevos componentes
- [**14-CONFLICTOS-WORDPRESS.md**](14-CONFLICTOS-WORDPRESS.md)
- Conflicto con .card
- Conflicto con form switches
- Conflicto con alineación vertical
- Checklist anti-conflictos
### 📚 RECURSOS Y EJEMPLOS
- [**15-EJEMPLOS-COMPLETOS.md**](15-EJEMPLOS-COMPLETOS.md)
- Ejemplos de campos completos
- Template básico
- Checklist de implementación
- Troubleshooting
### 🎨 TOKENS DE DISEÑO
- [**design-tokens-apus-admin.scss**](design-tokens-apus-admin.scss)
- Variables SCSS para colores
- Espaciado
- Tipografía
- Sombras y bordes
---
## 🚀 QUICK START
1. **Lee primero:** [01-ESPECIFICACIONES-DEL-SISTEMA.md](01-ESPECIFICACIONES-DEL-SISTEMA.md)
2. **Consulta el diseño:** [03-PALETA-DE-COLORES.md](03-PALETA-DE-COLORES.md) y [04-TIPOGRAFIA.md](04-TIPOGRAFIA.md)
3. **Usa los componentes:** [07-COMPONENTES-REUTILIZABLES.md](07-COMPONENTES-REUTILIZABLES.md)
4. **Implementa JavaScript:** [11-JAVASCRIPT-PATTERNS.md](11-JAVASCRIPT-PATTERNS.md)
5. **Resuelve conflictos WP:** [14-CONFLICTOS-WORDPRESS.md](14-CONFLICTOS-WORDPRESS.md)
---
## 📝 NOTAS IMPORTANTES
1.**Usar la librería de componentes HTML como referencia** (`../componentes-html-bootstrap/`)
2.**NUNCA modificar el HTML del front-end desde el admin panel**
3.**La vista previa debe ser 100% idéntica al componente real**
4.**Guardar configuración en archivos JSON (NO localStorage)**
5.**Responsive es obligatorio, no opcional**
6.**Todos los iconos deben ser orange (#FF8600)**
7.**Todos los títulos de card deben ser navy (#1e3a5f)**
---
**Documento creado:** 2025
**Versión:** 1.0
**Proyecto:** APU México - Sistema de Administración
**Autor:** Design System Team

View File

@@ -0,0 +1,358 @@
// =============================================================================
// APUS ADMIN PANEL - DESIGN TOKENS
// =============================================================================
// Este archivo contiene todas las variables de diseño del sistema de
// administración de APU México.
//
// Uso:
// @import 'design-tokens-apus-admin';
// =============================================================================
// =============================================================================
// COLORES
// =============================================================================
// Navy Brand Colors
// -----------------------------------------------------------------------------
$color-navy-dark: #0E2337; // Fondo principal, headers
$color-navy-primary: #1e3a5f; // Títulos, bordes importantes
$color-navy-light: #2c5282; // Variaciones secundarias
// Orange Accent Colors
// -----------------------------------------------------------------------------
$color-orange-primary: #FF8600; // Acción primaria, iconos destacados
$color-orange-hover: #FF6B35; // Hover states
$color-orange-light: #FFB800; // Badges, alerts suaves
// Neutral Colors
// -----------------------------------------------------------------------------
$color-neutral-50: #f8f9fa; // Fondo general del body
$color-neutral-100: #e9ecef; // Bordes, separadores
$color-neutral-200: #dee2e6; // Bordes sutiles
$color-neutral-600: #495057; // Texto principal
$color-neutral-700: #6c757d; // Texto secundario
$color-neutral-900: #212529; // Texto muy oscuro
// Semantic Colors
// -----------------------------------------------------------------------------
$color-success: #28a745; // Acciones exitosas
$color-danger: #dc3545; // Errores, acciones destructivas
$color-warning: #ffc107; // Advertencias
$color-info: #17a2b8; // Información
// Background Colors
// -----------------------------------------------------------------------------
$bg-body: $color-neutral-50;
$bg-card: #ffffff;
$bg-header: linear-gradient(135deg, $color-navy-dark 0%, $color-navy-primary 100%);
// =============================================================================
// TIPOGRAFÍA
// =============================================================================
// Font Family
// -----------------------------------------------------------------------------
$font-family-base: 'Poppins', sans-serif;
// Font Sizes
// -----------------------------------------------------------------------------
$font-size-base: 1rem; // 16px
$font-size-sm: 0.875rem; // 14px (labels, small text)
$font-size-xs: 0.75rem; // 12px (hints, badges)
$font-size-h1: 2rem; // 32px
$font-size-h2: 1.75rem; // 28px
$font-size-h3: 1.5rem; // 24px (título principal del tab)
$font-size-h4: 1.25rem; // 20px (subtítulos grandes)
$font-size-h5: 1rem; // 16px (títulos de cards)
$font-size-h6: 0.875rem; // 14px
// Font Weights
// -----------------------------------------------------------------------------
$font-weight-light: 300;
$font-weight-normal: 400;
$font-weight-medium: 500;
$font-weight-semibold: 600;
$font-weight-bold: 700;
// Line Heights
// -----------------------------------------------------------------------------
$line-height-base: 1.5;
$line-height-tight: 1.2; // Para títulos
$line-height-relaxed: 1.6;
// =============================================================================
// ESPACIADO
// =============================================================================
// Spacing Scale (rem)
// -----------------------------------------------------------------------------
$spacing-0: 0;
$spacing-1: 0.25rem; // 4px
$spacing-2: 0.5rem; // 8px
$spacing-3: 1rem; // 16px
$spacing-4: 1.5rem; // 24px
$spacing-5: 3rem; // 48px
// Specific Usage
// -----------------------------------------------------------------------------
$spacing-field-margin: $spacing-2; // mb-2 en campos
$spacing-card-margin: $spacing-3; // mb-3 en cards
$spacing-header-margin: $spacing-4; // mb-4 en headers
$spacing-icon-margin: $spacing-2; // me-2 en iconos grandes
$spacing-icon-small-margin: $spacing-1; // me-1 en iconos pequeños
$spacing-card-padding: $spacing-3; // p-3 en card-body
$spacing-container-padding: $spacing-4; // py-4 en container
// =============================================================================
// LAYOUT
// =============================================================================
// Container
// -----------------------------------------------------------------------------
$container-max-width: 1400px;
$container-padding-x: 0.75rem;
// Grid
// -----------------------------------------------------------------------------
$grid-gutter-width: 1.5rem;
$grid-gutter-sm: 1rem; // g-3
$grid-gutter-xs: 0.5rem; // g-2
// Breakpoints
// -----------------------------------------------------------------------------
$breakpoint-xs: 0;
$breakpoint-sm: 576px;
$breakpoint-md: 768px;
$breakpoint-lg: 992px; // Punto de quiebre principal
$breakpoint-xl: 1200px;
$breakpoint-xxl: 1400px;
// =============================================================================
// BORDES
// =============================================================================
// Border Width
// -----------------------------------------------------------------------------
$border-width-thin: 1px;
$border-width-thick: 4px;
// Border Radius
// -----------------------------------------------------------------------------
$border-radius-sm: 0.25rem; // 4px
$border-radius: 0.375rem; // 6px (default Bootstrap)
$border-radius-lg: 0.5rem; // 8px
// Border Colors
// -----------------------------------------------------------------------------
$border-color: $color-neutral-200;
$border-color-card: $color-navy-primary;
$border-color-preview: $color-orange-primary;
// =============================================================================
// SOMBRAS
// =============================================================================
$shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
$shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
$shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
// =============================================================================
// COMPONENTES
// =============================================================================
// Cards
// -----------------------------------------------------------------------------
$card-border-width: 0;
$card-border-left-width: $border-width-thick;
$card-border-radius: $border-radius;
$card-shadow: $shadow-sm;
$card-padding: $spacing-card-padding;
$card-margin: $spacing-card-margin;
// Buttons
// -----------------------------------------------------------------------------
$btn-border-radius: $border-radius;
$btn-padding-y: 0.375rem;
$btn-padding-x: 0.75rem;
$btn-padding-y-sm: 0.25rem;
$btn-padding-x-sm: 0.5rem;
// Inputs
// -----------------------------------------------------------------------------
$input-border-radius: $border-radius;
$input-border-color: $color-neutral-300;
$input-focus-border-color: $color-orange-primary;
$input-padding-y: 0.375rem;
$input-padding-x: 0.75rem;
$input-padding-y-sm: 0.25rem;
$input-padding-x-sm: 0.5rem;
// Badges
// -----------------------------------------------------------------------------
$badge-font-size: 0.65rem;
$badge-padding-y: 0.25rem;
$badge-padding-x: 0.5rem;
// Progress Bar
// -----------------------------------------------------------------------------
$progress-height: 3px;
$progress-bar-bg: $color-orange-primary;
$progress-bar-warning: $color-warning;
$progress-bar-danger: $color-danger;
// =============================================================================
// TRANSICIONES
// =============================================================================
$transition-base: all 0.3s ease-in-out;
$transition-fast: all 0.15s ease-in-out;
$transition-slow: all 0.5s ease-in-out;
// =============================================================================
// Z-INDEX
// =============================================================================
$z-index-notification: 9999;
$z-index-modal: 1050;
$z-index-dropdown: 1000;
$z-index-sticky: 1020;
// =============================================================================
// MIXINS
// =============================================================================
// Media Query Mixins
// -----------------------------------------------------------------------------
@mixin media-xs {
@media (max-width: #{$breakpoint-sm - 1px}) {
@content;
}
}
@mixin media-sm {
@media (min-width: $breakpoint-sm) {
@content;
}
}
@mixin media-md {
@media (min-width: $breakpoint-md) {
@content;
}
}
@mixin media-lg {
@media (min-width: $breakpoint-lg) {
@content;
}
}
@mixin media-xl {
@media (min-width: $breakpoint-xl) {
@content;
}
}
@mixin media-xxl {
@media (min-width: $breakpoint-xxl) {
@content;
}
}
// Component Mixins
// -----------------------------------------------------------------------------
@mixin card-base {
border: $card-border-width;
border-radius: $card-border-radius;
box-shadow: $card-shadow;
background-color: $bg-card;
margin-bottom: $card-margin;
}
@mixin card-border-left($color: $border-color-card) {
border-left: $border-width-thick solid $color;
}
@mixin text-truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@mixin center-flex {
display: flex;
align-items: center;
justify-content: center;
}
// =============================================================================
// UTILITY CLASSES (Opcional - si no usas Bootstrap utilities)
// =============================================================================
// Solo descomentar si necesitas clases que no están en Bootstrap
/*
.text-navy {
color: $color-navy-primary;
}
.text-orange {
color: $color-orange-primary;
}
.bg-navy {
background-color: $color-navy-primary;
}
.bg-orange {
background-color: $color-orange-primary;
}
.border-navy {
border-color: $color-navy-primary !important;
}
.border-orange {
border-color: $color-orange-primary !important;
}
*/
// =============================================================================
// EJEMPLO DE USO
// =============================================================================
/*
// En tu archivo SCSS:
@import 'design-tokens-apus-admin';
.custom-header {
background: $bg-header;
color: white;
padding: $spacing-card-padding;
border-left: $border-width-thick solid $color-orange-primary;
h1 {
font-size: $font-size-h3;
font-weight: $font-weight-bold;
margin-bottom: $spacing-1;
}
}
.custom-card {
@include card-base;
@include card-border-left($color-navy-primary);
&.preview {
@include card-border-left($color-orange-primary);
}
}
.responsive-text {
font-size: $font-size-sm;
@include media-lg {
font-size: $font-size-base;
}
}
*/

View File

@@ -0,0 +1,341 @@
# Análisis de Spam en Formularios - ROI Theme
**Fecha de análisis:** 2026-01-08
**Problema:** Recepción de spam con datos aleatorios en formularios
## Características del Spam Detectado
- **Nombres:** Cadenas aleatorias (ej: `kxUcwkDPHRAnUbdRWnDx`, `SOTbwKzTcZhJfTRBYSTV`)
- **WhatsApp:** Cadenas aleatorias en lugar de números
- **Emails:** Emails reales (posiblemente robados de bases de datos)
- **Patrón:** Bots automatizados que llenan campos sin validación
- **Fuente identificada:** `newsletter-footer`
## Módulos de Formularios Identificados
### Formulario 1: Newsletter Footer
- **Handler**: `Public/Footer/Infrastructure/Api/WordPress/NewsletterAjaxHandler.php`
- **Renderer**: `Public/Footer/Infrastructure/Ui/FooterRenderer.php`
- **Acción AJAX**: `roi_newsletter_subscribe`
- **Source en payload**: `newsletter-footer`
### Formulario 2: Contact Form (Sección + Modal)
- **Handler**: `Public/ContactForm/Infrastructure/Api/WordPress/ContactFormAjaxHandler.php`
- **Renderer**: `Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php`
- **Acción AJAX**: `roi_contact_form_submit`
- **Source en payload**: `contact-form`
---
## Hallazgos Durante la Revisión
### Archivos Identificados
**Schemas:**
- `Schemas/contact-form.json`
**JavaScript (Frontend):**
- `Assets/Js/footer-contact.js`
- `Assets/Js/modal-contact.js`
**PHP Backend - Newsletter (FUENTE DEL SPAM):**
- `Public/Footer/Infrastructure/Api/WordPress/NewsletterAjaxHandler.php`
- `Public/Footer/Infrastructure/Ui/FooterRenderer.php`
---
## Análisis del Formulario Newsletter (PRINCIPAL AFECTADO)
### Ubicación
- **Handler AJAX**: `Public/Footer/Infrastructure/Api/WordPress/NewsletterAjaxHandler.php`
- **Renderer HTML/JS**: `Public/Footer/Infrastructure/Ui/FooterRenderer.php`
- **Fuente en payload**: `newsletter-footer` ✓ (coincide con spam reportado)
### Medidas de Seguridad EXISTENTES ✅
| Medida | Estado | Archivo | Línea |
|--------|--------|---------|-------|
| Nonce WordPress | ✅ Implementado | NewsletterAjaxHandler.php | 46 |
| Rate Limiting (60s por IP) | ✅ Implementado | NewsletterAjaxHandler.php | 54-59 |
| Sanitización de inputs | ✅ Implementado | NewsletterAjaxHandler.php | 62-64 |
| Validación de email | ✅ Implementado | NewsletterAjaxHandler.php | 66 |
| Webhook URL oculta | ✅ Implementado | Nunca expuesta al cliente |
### VULNERABILIDADES CRÍTICAS ❌
| Vulnerabilidad | Impacto | Prioridad |
|----------------|---------|-----------|
| **NO hay honeypot** | Bots pasan sin detección | 🔴 ALTA |
| **NO hay CAPTCHA** | Sin verificación humana | 🔴 ALTA |
| **NO hay validación de nombre** | Acepta `kxUcwkDPHRAnUbdRWnDx` | 🔴 ALTA |
| **NO hay validación de WhatsApp** | Acepta `OGkrLENXqiQAaIYvCV` | 🔴 ALTA |
| **Rate limiting débil** | 60s es poco, bots rotan IPs | 🟡 MEDIA |
| **NO hay tiempo mínimo de envío** | Bots envían instantáneamente | 🟡 MEDIA |
### Campos del Formulario (FooterRenderer.php:336-343)
```html
<form id="roi-newsletter-form">
<input type="hidden" name="nonce" value="...">
<input type="text" name="name" placeholder="Nombre"> <!-- SIN VALIDACIÓN -->
<input type="email" name="email" placeholder="Email" required> <!-- SOLO HTML5 -->
<input type="tel" name="whatsapp" placeholder="WhatsApp"> <!-- SIN VALIDACIÓN -->
<button type="submit">Suscribirse</button>
</form>
```
### Patrón del Spam Detectado
Los datos de spam muestran:
- **Nombres**: Cadenas alfanuméricas aleatorias (20+ caracteres)
- **WhatsApp**: Cadenas alfanuméricas aleatorias (NO son números)
- **Emails**: Emails reales (posiblemente de bases de datos filtradas)
Esto indica **bots automatizados** que:
1. Bypassean la validación HTML5 (trivial)
2. Ignoran el rate limiting rotando IPs
3. Generan valores aleatorios para campos de texto
4. Usan emails reales para parecer legítimos
---
## Análisis del Formulario Contact Form (SEGUNDO FORMULARIO)
### Ubicación
- **Handler AJAX**: `Public/ContactForm/Infrastructure/Api/WordPress/ContactFormAjaxHandler.php`
- **Renderer**: `Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php`
- **Schema**: `Schemas/contact-form.json`
- **Fuente en payload**: `contact-form`
### Estado: ✅ ACTIVO (Clean Architecture)
Este formulario está implementado correctamente con Clean Architecture:
- Renderer genera HTML/CSS/JS dinámicamente
- Handler AJAX procesa envíos server-side
- Webhook URL guardada en BD (nunca expuesta al cliente)
### Medidas de Seguridad EXISTENTES ✅
| Medida | Estado | Archivo | Línea |
|--------|--------|---------|-------|
| Nonce WordPress | ✅ Implementado | ContactFormAjaxHandler.php | 47-48 |
| Rate Limiting (30s por IP) | ✅ Implementado | ContactFormAjaxHandler.php | 55-61 |
| Sanitización de inputs | ✅ Implementado | ContactFormAjaxHandler.php | 122-130 |
| Validación de email | ✅ Implementado | ContactFormAjaxHandler.php | 151-155 |
| Webhook URL oculta | ✅ Implementado | ContactFormAjaxHandler.php | 86 |
### VULNERABILIDADES CRÍTICAS ❌
| Vulnerabilidad | Impacto | Prioridad |
|----------------|---------|-----------|
| **NO hay honeypot** | Bots pasan sin detección | 🔴 ALTA |
| **NO hay CAPTCHA** | Sin verificación humana | 🔴 ALTA |
| **NO hay validación de nombre** | Acepta `kxUcwkDPHRAnUbdRWnDx` | 🔴 ALTA |
| **NO hay validación de WhatsApp** | Acepta `OGkrLENXqiQAaIYvCV` | 🔴 ALTA |
| **Rate limiting muy débil** | Solo 30s, bots rotan IPs | 🟡 MEDIA |
| **NO hay tiempo mínimo de envío** | Bots envían instantáneamente | 🟡 MEDIA |
### Campos del Formulario (ContactFormRenderer.php:278-320)
```html
<form id="roiContactForm" data-nonce="...">
<input type="text" name="fullName" placeholder="Nombre completo *" required> <!-- SIN VALIDACIÓN FORMATO -->
<input type="text" name="company" placeholder="Empresa">
<input type="tel" name="whatsapp" placeholder="WhatsApp *" required> <!-- SIN VALIDACIÓN FORMATO -->
<input type="email" name="email" placeholder="Correo electrónico *" required> <!-- SOLO required + is_email -->
<textarea name="message"></textarea>
</form>
```
### Nota sobre archivo Legacy (footer-contact.js)
El archivo `Assets/Js/footer-contact.js` es **código legacy NO utilizado**:
- NO está encolado en `Inc/enqueue-scripts.php`
- Tiene webhook URL hardcodeada (mala práctica)
- Fue reemplazado por el sistema Clean Architecture actual
---
## Resumen Comparativo de Ambos Formularios
| Característica | Newsletter Footer | Contact Form |
|----------------|-------------------|--------------|
| **Estado** | ✅ ACTIVO | ✅ ACTIVO |
| **Backend** | PHP AJAX | PHP AJAX |
| **Nonce** | ✅ Sí | ✅ Sí |
| **Rate Limit** | ✅ 60s | ✅ 30s |
| **Sanitización** | ✅ Sí | ✅ Sí |
| **Validación email** | ✅ is_email() | ✅ is_email() |
| **Validación nombre** | ❌ Ninguna | ❌ Solo required |
| **Validación WhatsApp** | ❌ Ninguna | ❌ Solo required |
| **Honeypot** | ❌ NO | ❌ NO |
| **CAPTCHA** | ❌ NO | ❌ NO |
| **Tiempo mínimo** | ❌ NO | ❌ NO |
### Conclusión: AMBOS formularios comparten las MISMAS vulnerabilidades críticas
---
## Soluciones Anti-Spam Propuestas
### PRIORIDAD 1: Newsletter Footer (Urgente)
#### 1.1 Honeypot Field (Implementación rápida, efectiva)
```html
<!-- Campo oculto que humanos no llenan pero bots sí -->
<input type="text" name="website_url" style="display:none !important" tabindex="-1" autocomplete="off">
```
Backend (`NewsletterAjaxHandler.php`):
```php
// Verificar honeypot
$honeypot = sanitize_text_field($_POST['website_url'] ?? '');
if (!empty($honeypot)) {
// Es bot - responder éxito falso para no alertar
wp_send_json_success(['message' => $successMsg]);
return;
}
```
#### 1.2 Validación de Contenido (Anti-gibberish)
```php
// Validar nombre: solo letras, espacios, acentos
$name = sanitize_text_field($_POST['name'] ?? '');
if (!empty($name) && !preg_match('/^[\p{L}\s\'-]{2,50}$/u', $name)) {
wp_send_json_error(['message' => 'Nombre inválido'], 422);
return;
}
// Validar WhatsApp: solo números, +, -, espacios
$whatsapp = sanitize_text_field($_POST['whatsapp'] ?? '');
if (!empty($whatsapp) && !preg_match('/^[\d\s\+\-\(\)]{10,20}$/', $whatsapp)) {
wp_send_json_error(['message' => 'Número de WhatsApp inválido'], 422);
return;
}
```
#### 1.3 Tiempo Mínimo de Envío
```php
// En el formulario HTML agregar:
<input type="hidden" name="form_timestamp" value="<?php echo time(); ?>">
// En el handler:
$timestamp = (int) ($_POST['form_timestamp'] ?? 0);
$minTime = 3; // segundos mínimos
if (time() - $timestamp < $minTime) {
// Envío demasiado rápido = bot
wp_send_json_success(['message' => $successMsg]); // Falso éxito
return;
}
```
#### 1.4 Rate Limiting Mejorado
```php
// Aumentar de 60s a 300s (5 minutos)
// Y agregar límite diario por IP
private function checkRateLimit(): bool
{
$ip = $this->getClientIP();
// Límite corto (5 minutos)
$shortKey = 'roi_newsletter_short_' . md5($ip);
if (get_transient($shortKey) !== false) {
return false;
}
set_transient($shortKey, time(), 300);
// Límite diario (máximo 5 por día)
$dailyKey = 'roi_newsletter_daily_' . md5($ip);
$dailyCount = (int) get_transient($dailyKey);
if ($dailyCount >= 5) {
return false;
}
set_transient($dailyKey, $dailyCount + 1, DAY_IN_SECONDS);
return true;
}
```
### PRIORIDAD 2: Opciones Adicionales
#### 2.1 Google reCAPTCHA v3 (Invisible)
- Sin fricción para usuarios
- Score basado en comportamiento
- Requiere cuenta Google reCAPTCHA
#### 2.2 Cloudflare Turnstile (Recomendado)
- Gratuito
- Sin tracking de Google
- Más privado que reCAPTCHA
#### 2.3 hCaptcha
- Alternativa a reCAPTCHA
- Mejor privacidad
- Versión gratuita disponible
---
## Plan de Implementación
### Fase 1: Crear Servicio Anti-Spam Compartido
1. Crear `Shared/Infrastructure/Services/AntiSpamValidator.php`
2. Implementar validaciones reutilizables:
- `validateHoneypot(string $value): bool`
- `validateMinimumTime(int $timestamp, int $minSeconds = 3): bool`
- `validateNameFormat(string $name): bool` (regex letras/espacios)
- `validateWhatsAppFormat(string $phone): bool` (regex números)
- `checkEnhancedRateLimit(string $ip, int $shortLimit, int $dailyLimit): bool`
### Fase 2: Aplicar a Newsletter Footer
1. Agregar honeypot + timestamp en `FooterRenderer.php`
2. Integrar AntiSpamValidator en `NewsletterAjaxHandler.php`
3. Probar que spam es rechazado
### Fase 3: Aplicar a Contact Form
1. Agregar honeypot + timestamp en `ContactFormRenderer.php`
2. Integrar AntiSpamValidator en `ContactFormAjaxHandler.php`
3. Probar que spam es rechazado
### Fase 4: Monitoreo y Mejoras (Opcional)
1. Agregar logging de intentos sospechosos
2. Evaluar CAPTCHA invisible si spam persiste
3. Dashboard de estadísticas anti-spam
---
## Archivos a Modificar
### Newsletter Footer
| Archivo | Cambio |
|---------|--------|
| `Public/Footer/Infrastructure/Ui/FooterRenderer.php` | Agregar honeypot + timestamp oculto |
| `Public/Footer/Infrastructure/Api/WordPress/NewsletterAjaxHandler.php` | Validaciones formato + honeypot check + rate limiting mejorado |
### Contact Form
| Archivo | Cambio |
|---------|--------|
| `Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php` | Agregar honeypot + timestamp oculto |
| `Public/ContactForm/Infrastructure/Api/WordPress/ContactFormAjaxHandler.php` | Validaciones formato + honeypot check + rate limiting mejorado |
### Código Reutilizable (Recomendado)
Crear un trait o clase helper compartida para evitar duplicación:
```
Shared/Infrastructure/Services/AntiSpamValidator.php
```
- Validación de honeypot
- Validación de tiempo mínimo
- Validación de formato nombre (regex)
- Validación de formato WhatsApp (regex)
- Rate limiting mejorado
---
## Referencias Serena
Las memorias existentes en `.serena/Memories/` documentan:
- `diagnostico-estructura-carpetas-admin.md` - Estructura de carpetas Admin
- `migracion-theme-options-tabla-personalizada.md` - Sistema de settings
- `ROI_THEME_CSS_LOADING_ANALYSIS.md` - Análisis de carga CSS
Esta memoria debe guardarse como referencia para implementación futura.

View File

@@ -0,0 +1,366 @@
# Plan de Mejora de Especificaciones OpenSpec - ROI Theme
**Fecha:** 2026-01-08
**Referencia:** Modelo de especificaciones de Aditec (`D:\_Desarrollo\100Aditec\_openspec`)
---
## INSTRUCCIONES DE RECUPERACIÓN (LEER PRIMERO)
> **Si la conversación se compactó, se fue el internet o se apagó la computadora:**
1. **Estado actual:** Ver sección "ESTADO ACTUAL" abajo
2. **Buscar primer `[ ]`:** Ir a la tarea sin marcar
3. **Continuar desde ahí:** No repetir tareas marcadas `[x]`
4. **Marcar `[x]` al completar:** Guardar después de cada cambio
### ESTADO ACTUAL
| Campo | Valor |
|-------|-------|
| **Última actualización** | 2026-01-08 |
| **Última tarea completada** | FASE 3 - Referencias y verificación COMPLETADO |
| **Próxima tarea** | CASO PILOTO P.1 - Crear spec para AntiSpamValidator |
| **Progreso total** | 66/75 tareas |
### Archivos de contexto a leer si es necesario:
- `_planificacion/analisis-spam-formularios.md` - Análisis de spam
- `_planificacion/validacion-specs-vs-codigo-actual.md` - Validación código vs specs
- `_openspec/specs/` - Specs base del proyecto (3 archivos)
- `_openspec/changes/` - Specs de features específicos
- `D:\_Desarrollo\100Aditec\_openspec\` - Referencia de Aditec
---
## CHECKLIST MAESTRO DE PROGRESO
### Estado General
- [x] Análisis de especificaciones Aditec completado
- [x] Comparación ROI Theme vs Aditec completado
- [x] Validación código actual vs especificaciones propuestas
- [x] **FASE 1: Crear archivos faltantes** (COMPLETADO)
- [x] **FASE 2: Expandir archivos existentes** (COMPLETADO)
- [x] **FASE 3: Actualizar referencias** (COMPLETADO)
- [ ] **CASO PILOTO: Implementar anti-spam con nuevo sistema**
---
## FASE 1: CREAR ARCHIVOS FALTANTES
### 1.1 WORKFLOW-ROI-THEME.md (~300 líneas)
**Ubicación:** `_openspec/WORKFLOW-ROI-THEME.md`
**Estado:** [x] NO INICIADO | [x] EN PROGRESO | [x] COMPLETADO
#### Secciones a crear:
- [x] **1.1.1** Encabezado y metadata (version, fecha)
- [x] **1.1.2** Regla de Oro del proyecto
- [x] "SI NO EXISTE spec.md → NO SE TOCA CÓDIGO"
- [x] Explicación de por qué esta regla
- [x] **1.1.3** Flujo de 3 fases obligatorio
- [x] Fase 1: Proponer (qué archivos se crean)
- [x] Fase 2: Especificar (qué archivos se crean)
- [x] Fase 3: Implementar (qué archivos se crean)
- [x] **1.1.4** Flujo de 5 fases para componentes ROI Theme
- [x] Fase 1: Schema JSON (ubicación, formato, campos obligatorios)
- [x] Fase 2: Sincronización BD (comando WP-CLI)
- [x] Fase 3: Renderer (ubicación, patrones, DI)
- [x] Fase 4: FormBuilder (ubicación, patrones, Design System)
- [x] Fase 5: Validación (script, qué valida)
- [x] **1.1.5** Agentes disponibles y cuándo usarlos
- [x] roi-schema-architect
- [x] roi-renderer-builder
- [x] roi-form-builder
- [x] **1.1.6** Comandos WP-CLI disponibles
- [x] sync-component
- [x] sync-all-components
- [x] Ejemplos de uso
- [x] **1.1.7** Reglas de Clean Architecture resumidas
- [x] Qué puede y no puede hacer Domain
- [x] Qué puede y no puede hacer Application
- [x] Qué puede y no puede hacer Infrastructure
- [x] **1.1.8** Sección de Lecciones Aprendidas
- [x] Errores comunes y cómo evitarlos
- [x] Patrones que funcionan
---
### 1.2 nomenclatura.md (~600 líneas)
**Ubicación:** `_openspec/specs/nomenclatura.md`
**Estado:** [x] NO INICIADO | [x] EN PROGRESO | [x] COMPLETADO
#### Pre-requisitos:
- [x] **1.2.0** Archivo plano en `_openspec/specs/` (sin carpeta anidada)
#### Secciones a crear:
- [x] **1.2.1** Encabezado y Feature principal
- [x] **1.2.2** Tabla resumen de TODAS las nomenclaturas (vista rápida)
- [x] **1.2.3** Nomenclatura de Carpetas
- [x] Carpetas principales (Admin/, Public/, Shared/)
- [x] Carpetas de módulo (PascalCase)
- [x] Carpetas auxiliares (_planificacion, _arquitectura)
- [x] Carpetas de capas (Domain/, Application/, Infrastructure/)
- [x] Ejemplos correctos e incorrectos
- [x] **1.2.4** Nomenclatura de Archivos PHP
- [x] Archivos de clase (PascalCase.php)
- [x] Archivos de interface (PascalCaseInterface.php)
- [x] Archivos de trait (PascalCaseTrait.php)
- [x] Ejemplos correctos e incorrectos
- [x] **1.2.5** Nomenclatura de Archivos JSON (Schemas)
- [x] Nombres en kebab-case
- [x] Extensión .json
- [x] Ejemplos correctos e incorrectos
- [x] **1.2.6** Nomenclatura de Namespaces
- [x] Patrón: ROITheme\[Context]\[Component]\[Layer]
- [x] Context: Admin, Public, Shared
- [x] Layer: Domain, Application, Infrastructure
- [x] Subcapas: Ui, Api, Persistence, Services
- [x] Ejemplos completos
- [x] **1.2.7** Nomenclatura de Clases
- [x] Clases regulares (PascalCase)
- [x] Renderers ([Component]Renderer)
- [x] FormBuilders ([Component]FormBuilder)
- [x] UseCases ([Action][Entity]UseCase)
- [x] Services ([Entity]Service)
- [x] Repositories ([Entity]Repository)
- [x] Handlers ([Action]Handler)
- [x] Ejemplos correctos e incorrectos
- [x] **1.2.8** Nomenclatura de Interfaces
- [x] Sufijo Interface o Contract
- [x] Ubicación en Domain/Contracts/
- [x] Ejemplos correctos e incorrectos
- [x] **1.2.9** Nomenclatura de Métodos
- [x] Métodos públicos (camelCase)
- [x] Métodos privados (camelCase)
- [x] Métodos booleanos (is*, has*, can*, should*)
- [x] Getters/Setters (get*, set*)
- [x] Ejemplos correctos e incorrectos
- [x] **1.2.10** Nomenclatura de Propiedades
- [x] Propiedades (camelCase)
- [x] Propiedades booleanas (is*, has*)
- [x] Visibilidad (private por defecto)
- [x] Ejemplos correctos e incorrectos
- [x] **1.2.11** Nomenclatura de Variables
- [x] Variables locales ($camelCase)
- [x] Parámetros ($camelCase)
- [x] Variables de iteración ($item, $key, $i)
- [x] Ejemplos correctos e incorrectos
- [x] **1.2.12** Nomenclatura de Constantes
- [x] Constantes de clase (UPPER_SNAKE_CASE)
- [x] Constantes globales (ROI_THEME_*)
- [x] Ejemplos correctos e incorrectos
- [x] **1.2.13** Nomenclatura de component_name
- [x] Formato kebab-case en JSON
- [x] Formato kebab-case en BD
- [x] Conversión kebab-case ↔ PascalCase
- [x] Ejemplos de conversión
- [x] **1.2.14** Nomenclatura de Hooks WordPress
- [x] Actions (roi_theme_*)
- [x] Filters (roi_theme_filter_*)
- [x] Ejemplos correctos e incorrectos
- [x] **1.2.15** Validación Pre-commit
- [x] Checklist de nomenclatura
- [x] Errores comunes a evitar
---
## FASE 2: EXPANDIR ARCHIVOS EXISTENTES
### 2.1 arquitectura-limpia.md (+485 líneas)
**Ubicación:** `_openspec/specs/arquitectura-limpia.md`
**Estado:** [x] NO INICIADO | [x] EN PROGRESO | [x] COMPLETADO
#### Secciones a agregar:
- [x] **2.1.1** Diagrama ASCII de las 3 capas
- [x] Domain (centro, sin dependencias)
- [x] Application (usa Domain)
- [x] Infrastructure (usa Application y Domain)
- [x] Flechas de dependencia
- [x] **2.1.2** Estructura completa de carpetas del tema
- [x] Árbol completo roi-theme/
- [x] Explicación de cada carpeta
- [x] Cuándo crear nuevas carpetas
- [x] **2.1.3** Reglas de anidamiento
- [x] Profundidad máxima: 4 niveles
- [x] Regla de 3 archivos para subcarpetas
- [x] Ejemplos de estructuras válidas e inválidas
- [x] **2.1.4** Diferencia entre niveles de Shared
- [x] Shared/ (raíz) - código global
- [x] Admin/Shared/ - compartido solo en Admin
- [x] Public/Shared/ - compartido solo en Public
- [x] Cuándo usar cada uno
- [x] **2.1.5** Ejemplos de código PHP por capa
- [x] Domain: Entidad correcta vs incorrecta
- [x] Domain: Interface correcta vs incorrecta
- [x] Application: UseCase correcto vs incorrecto
- [x] Infrastructure: Repository correcto vs incorrecto
- [x] Infrastructure: Renderer correcto vs incorrecto
- [x] **2.1.6** Patrones de herencia
- [x] Herencia para Renderers (si aplica)
- [x] Herencia para FormBuilders (si aplica)
- [x] Composición vs Herencia
- [x] Profundidad máxima de herencia
- [x] **2.1.7** Mapeo de terminología
- [x] Tabla: Clean Architecture estándar → ROI Theme
- [x] Entity → Component/Entity
- [x] UseCase → UseCase
- [x] Gateway → Repository
- [x] Presenter → Renderer
- [x] **2.1.8** Validación de arquitectura
- [x] Script validate-architecture.php
- [x] Qué valida
- [x] Cómo ejecutarlo
- [x] Cómo interpretar errores
---
### 2.2 estandares-codigo.md (+877 líneas)
**Ubicación:** `_openspec/specs/estandares-codigo.md`
**Estado:** [x] NO INICIADO | [x] EN PROGRESO | [x] COMPLETADO
#### Secciones a agregar:
- [x] **2.2.1** Ejemplos de código PHP para SOLID
- [x] SRP: Ejemplo correcto vs incorrecto
- [x] OCP: Ejemplo correcto vs incorrecto
- [x] LSP: Ejemplo correcto vs incorrecto
- [x] ISP: Ejemplo correcto vs incorrecto
- [x] DIP: Ejemplo correcto vs incorrecto
- [x] **2.2.2** Manejo de errores WordPress
- [x] Cuándo usar wp_die()
- [x] Cuándo usar WP_Error
- [x] Cuándo usar excepciones
- [x] Ejemplos de cada caso
- [x] **2.2.3** Sanitización y validación
- [x] sanitize_text_field()
- [x] sanitize_email()
- [x] absint()
- [x] wp_kses()
- [x] Tabla de funciones por tipo de dato
- [x] **2.2.4** Escaping para output
- [x] esc_html()
- [x] esc_attr()
- [x] esc_url()
- [x] esc_textarea()
- [x] wp_kses_post()
- [x] Tabla de funciones por contexto
- [x] **2.2.5** Hooks WordPress
- [x] add_action() - cuándo y cómo
- [x] add_filter() - cuándo y cómo
- [x] Prioridades
- [x] Número de argumentos
- [x] Ejemplos correctos
- [x] **2.2.6** Recursos y cleanup
- [x] Transients (set, get, delete)
- [x] Object cache
- [x] Cuándo limpiar recursos
- [x] Ejemplos
- [x] **2.2.7** Checklist pre-commit detallado
- [x] Verificaciones de sintaxis
- [x] Verificaciones de estilo
- [x] Verificaciones de seguridad
- [x] Verificaciones de arquitectura
- [x] Verificaciones de nomenclatura
---
## FASE 3: ACTUALIZAR REFERENCIAS
### 3.1 Actualizar project.md
**Ubicación:** `_openspec/project.md`
**Estado:** [x] NO INICIADO | [x] EN PROGRESO | [x] COMPLETADO
- [x] **3.1.1** Agregar referencia a WORKFLOW-ROI-THEME.md
- [x] **3.1.2** Agregar referencia a nomenclatura/spec.md
- [x] **3.1.3** Actualizar índice de specs
- [x] **3.1.4** Verificar que links funcionan
### 3.2 Cross-references entre specs
**Estado:** [x] NO INICIADO | [x] EN PROGRESO | [x] COMPLETADO
- [x] **3.2.1** arquitectura-limpia → nomenclatura (referencias a nombres)
- [x] **3.2.2** estandares-codigo → arquitectura-limpia (referencias a capas)
- [x] **3.2.3** nomenclatura → estandares-codigo (referencias a convenciones)
- [x] **3.2.4** WORKFLOW → todas las specs (referencias a cada fase)
### 3.3 Verificación final
**Estado:** [x] NO INICIADO | [x] EN PROGRESO | [x] COMPLETADO
- [x] **3.3.1** Consistencia de ejemplos entre archivos
- [x] **3.3.2** Todos los links funcionan
- [x] **3.3.3** No hay contradicciones entre specs
- [x] **3.3.4** Todos los archivos tienen fecha de actualización
---
## CASO PILOTO: ANTI-SPAM
### Implementación usando nuevo sistema de specs
**Estado:** [ ] NO INICIADO | [ ] EN PROGRESO | [ ] COMPLETADO
- [ ] **P.1** Crear spec para AntiSpamValidator siguiendo WORKFLOW
- [ ] **P.2** Validar que spec cumple nomenclatura
- [ ] **P.3** Validar que spec cumple arquitectura-limpia
- [ ] **P.4** Validar que spec cumple estandares-codigo
- [ ] **P.5** Implementar AntiSpamValidator
- [ ] **P.6** Aplicar a Newsletter Footer
- [ ] **P.7** Aplicar a Contact Form
- [ ] **P.8** Probar que spam es rechazado
- [ ] **P.9** Documentar lecciones aprendidas en WORKFLOW
---
## RESUMEN DE ARCHIVOS A CREAR/MODIFICAR
| # | Archivo | Acción | Líneas | Estado |
|---|---------|--------|--------|--------|
| 1 | `_openspec/WORKFLOW-ROI-THEME.md` | CREAR | ~438 | [x] COMPLETADO |
| 2 | `_openspec/specs/nomenclatura.md` | CREAR | ~687 | [x] COMPLETADO |
| 3 | `_openspec/specs/arquitectura-limpia.md` | EXPANDIR | ~770 | [x] COMPLETADO |
| 4 | `_openspec/specs/estandares-codigo.md` | EXPANDIR | ~1228 | [x] COMPLETADO |
| 5 | `_openspec/project.md` | ACTUALIZAR | ~107 | [x] COMPLETADO |
### Estructura Final
```
_openspec/
├── AGENTS.md
├── WORKFLOW-ROI-THEME.md
├── project.md
├── specs/ # Solo specs BASE
│ ├── arquitectura-limpia.md
│ ├── estandares-codigo.md
│ └── nomenclatura.md
└── changes/ # Features/cambios específicos
├── add-advanced-incontent-ads/
├── adsense-auto-ads-toggle/
├── adsense-cache-unified-visibility/
├── adsense-javascript-first/
├── cache-first-architecture/
├── flujo-componentes/
├── patrones-wordpress/
├── post-grid-shortcode/
├── refactor-adsense-lazy-loading/
└── templates-unificados/
```
---
## MÉTRICAS DE PROGRESO
### Contadores actuales:
- **Tareas FASE 1:** 32/32 completadas
- **Tareas FASE 2:** 23/23 completadas
- **Tareas FASE 3:** 11/11 completadas
- **Tareas PILOTO:** 0/9 completadas
- **TOTAL:** 66/75 tareas completadas
### Al completar:
| Métrica | Antes | Después |
|---------|-------|---------|
| Total líneas specs | ~1021 | ~2200 |
| Archivos fundamentales | 3/5 | 5/5 |
| Diagramas ASCII | 0 | 3+ |
| Ejemplos código | ~5 | ~30 |
| Checklists | 1 parcial | 3 completos |
---
**Fin del documento**

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,317 @@
/**
* APU MÉXICO - MAIN JAVASCRIPT
*/
// Navbar scroll effect
window.addEventListener('scroll', function() {
const navbar = document.querySelector('.navbar');
if (navbar) {
if (window.scrollY > 50) {
navbar.classList.add('scrolled');
} else {
navbar.classList.remove('scrolled');
}
}
});
// Table of Contents - ScrollSpy
function updateActiveSection() {
const tocLinks = document.querySelectorAll('.toc-container a');
if (!tocLinks.length) return;
const navbar = document.querySelector('.navbar');
const navbarHeight = navbar ? navbar.offsetHeight : 0;
const sectionIds = Array.from(tocLinks).map(link => {
const href = link.getAttribute('href');
return href ? href.substring(1) : null;
}).filter(id => id !== null);
const sections = sectionIds.map(id => document.getElementById(id)).filter(el => el !== null);
const scrollPosition = window.scrollY + navbarHeight + 100;
let activeSection = null;
for (let i = 0; i < sections.length; i++) {
const section = sections[i];
const sectionTop = section.offsetTop;
if (scrollPosition >= sectionTop) {
activeSection = section.getAttribute('id');
} else {
break;
}
}
tocLinks.forEach(link => {
link.classList.remove('active');
const href = link.getAttribute('href');
if (href === '#' + activeSection) {
link.classList.add('active');
}
});
}
// Smooth scroll for TOC links
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.toc-container a').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const targetId = this.getAttribute('href');
const targetElement = document.querySelector(targetId);
if (targetElement) {
const navbar = document.querySelector('.navbar');
const navbarHeight = navbar ? navbar.offsetHeight : 0;
const offsetTop = targetElement.offsetTop - navbarHeight - 40;
window.scrollTo({
top: offsetTop,
behavior: 'smooth'
});
}
});
});
updateActiveSection();
});
window.addEventListener('scroll', updateActiveSection);
// A/B Testing for CTA sections
document.addEventListener('DOMContentLoaded', function() {
const ctaVariant = Math.random() < 0.5 ? 'A' : 'B';
if (ctaVariant === 'A') {
const variantA = document.querySelector('.cta-variant-a');
if (variantA) variantA.style.display = 'block';
} else {
const variantB = document.querySelector('.cta-variant-b');
if (variantB) variantB.style.display = 'block';
}
document.querySelectorAll('.cta-button').forEach(button => {
button.addEventListener('click', function() {
const variant = this.getAttribute('data-cta-variant');
console.log('CTA clicked - Variant: ' + variant);
if (typeof gtag !== 'undefined') {
gtag('event', 'cta_click', {
'event_category': 'CTA',
'event_label': 'Variant_' + variant,
'value': variant
});
}
});
});
});
// Contact Modal - Dynamic Loading
function loadContactModal() {
const modalContainer = document.getElementById('modalContainer');
if (!modalContainer) return;
fetch('modal-contact.html')
.then(response => response.text())
.then(html => {
modalContainer.innerHTML = html;
initContactForm();
})
.catch(error => {
console.error('Error loading modal:', error);
});
}
document.addEventListener('DOMContentLoaded', loadContactModal);
// Contact Form - Webhook Submission
function initContactForm() {
const form = document.getElementById('contactForm');
if (!form) return;
form.addEventListener('submit', function(e) {
e.preventDefault();
const WEBHOOK_URL = 'https://tu-webhook.com/contacto';
const formData = {
fullName: document.getElementById('fullName').value,
company: document.getElementById('company').value,
whatsapp: document.getElementById('whatsapp').value,
email: document.getElementById('email').value,
comments: document.getElementById('comments').value,
timestamp: new Date().toISOString(),
source: 'APU Website - Modal'
};
if (!formData.fullName || !formData.whatsapp || !formData.email) {
showFormMessage('Por favor completa todos los campos requeridos', 'danger');
return;
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(formData.email)) {
showFormMessage('Por favor ingresa un correo electrónico válido', 'danger');
return;
}
const submitButton = form.querySelector('button[type="submit"]');
const originalText = submitButton.innerHTML;
submitButton.disabled = true;
submitButton.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Enviando...';
fetch(WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
})
.then(response => {
if (!response.ok) throw new Error('Error en el envío');
return response.json();
})
.then(data => {
showFormMessage('¡Mensaje enviado exitosamente!', 'success');
form.reset();
if (typeof gtag !== 'undefined') {
gtag('event', 'form_submission', {
'event_category': 'Contact Form',
'event_label': 'Form Submitted'
});
}
setTimeout(() => {
const modal = bootstrap.Modal.getInstance(document.getElementById('contactModal'));
if (modal) modal.hide();
}, 2000);
})
.catch(error => {
showFormMessage('Error al enviar el mensaje', 'danger');
})
.finally(() => {
submitButton.disabled = false;
submitButton.innerHTML = originalText;
});
});
}
function showFormMessage(message, type) {
const messageDiv = document.getElementById('formMessage');
if (!messageDiv) return;
messageDiv.textContent = message;
messageDiv.className = `mt-3 alert alert-${type}`;
messageDiv.style.display = 'block';
setTimeout(() => {
messageDiv.style.display = 'none';
}, 5000);
}
// Footer Contact Form
document.addEventListener('DOMContentLoaded', function() {
const footerForm = document.getElementById('footerContactForm');
if (!footerForm) return;
footerForm.addEventListener('submit', function(e) {
e.preventDefault();
const WEBHOOK_URL = 'https://tu-webhook.com/contacto';
const formData = {
fullName: document.getElementById('footerFullName').value,
company: document.getElementById('footerCompany').value,
whatsapp: document.getElementById('footerWhatsapp').value,
email: document.getElementById('footerEmail').value,
comments: document.getElementById('footerComments').value,
timestamp: new Date().toISOString(),
source: 'APU Website - Footer'
};
if (!formData.fullName || !formData.whatsapp || !formData.email) {
showFooterFormMessage('Por favor completa todos los campos requeridos', 'danger');
return;
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(formData.email)) {
showFooterFormMessage('Por favor ingresa un correo válido', 'danger');
return;
}
const submitButton = footerForm.querySelector('button[type="submit"]');
const originalText = submitButton.innerHTML;
submitButton.disabled = true;
submitButton.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Enviando...';
fetch(WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
})
.then(response => {
if (!response.ok) throw new Error('Error en el envío');
return response.json();
})
.then(data => {
showFooterFormMessage('¡Mensaje enviado exitosamente!', 'success');
footerForm.reset();
if (typeof gtag !== 'undefined') {
gtag('event', 'form_submission', {
'event_category': 'Footer Form',
'event_label': 'Form Submitted'
});
}
})
.catch(error => {
showFooterFormMessage('Error al enviar el mensaje', 'danger');
})
.finally(() => {
submitButton.disabled = false;
submitButton.innerHTML = originalText;
});
});
});
function showFooterFormMessage(message, type) {
const messageDiv = document.getElementById('footerFormMessage');
if (!messageDiv) return;
messageDiv.textContent = message;
messageDiv.className = `col-12 mt-2 alert alert-${type}`;
messageDiv.style.display = 'block';
setTimeout(() => {
messageDiv.style.display = 'none';
}, 5000);
}
// Smooth scroll for all anchor links
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
const href = this.getAttribute('href');
if (href === '#' || this.getAttribute('data-bs-toggle') === 'modal') {
return;
}
const targetElement = document.querySelector(href);
if (!targetElement) return;
e.preventDefault();
const navbar = document.querySelector('.navbar');
const navbarHeight = navbar ? navbar.offsetHeight : 0;
const offsetTop = targetElement.offsetTop - navbarHeight - 20;
window.scrollTo({
top: offsetTop,
behavior: 'smooth'
});
});
});
});
console.log('%c APU México ', 'background: #1e3a5f; color: #FF8600; font-size: 16px; font-weight: bold; padding: 10px;');

View File

@@ -0,0 +1,42 @@
<!-- Contact Modal -->
<div class="modal fade" id="contactModal" tabindex="-1" aria-labelledby="contactModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header border-0">
<h5 class="modal-title fw-bold" id="contactModalLabel">¿Listo para comenzar?</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body px-4 pb-4">
<p class="text-muted mb-4">Completa el formulario y nos pondremos en contacto contigo lo antes posible.</p>
<form id="contactForm">
<div class="mb-3">
<label for="fullName" class="form-label">Nombre completo <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="fullName" name="fullName" required>
</div>
<div class="mb-3">
<label for="company" class="form-label">Empresa</label>
<input type="text" class="form-control" id="company" name="company">
</div>
<div class="mb-3">
<label for="whatsapp" class="form-label">WhatsApp <span class="text-danger">*</span></label>
<input type="tel" class="form-control" id="whatsapp" name="whatsapp" placeholder="+52 ___ ___ ____" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">Correo electrónico <span class="text-danger">*</span></label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="mb-3">
<label for="comments" class="form-label">Comentarios</label>
<textarea class="form-control" id="comments" name="comments" rows="3"></textarea>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-submit-form btn-lg">
Enviar
</button>
</div>
<div id="formMessage" class="mt-3 alert" style="display: none;"></div>
</form>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,335 @@
# Validación: Especificaciones vs Código Actual
**Fecha:** 2026-01-08
**Objetivo:** Verificar que las especificaciones reflejan el código existente
---
## Resumen Ejecutivo
**CONCLUSIÓN: El código actual YA cumple con las especificaciones propuestas.**
Las mejoras a las especificaciones son documentación/clarificación de patrones existentes, NO nuevas invenciones.
---
## 1. Arquitectura Clean Architecture
### Verificación de Estructura de Capas
| Capa | Ubicación Esperada | Estado |
|------|-------------------|--------|
| Domain | `*/Domain/` | ✅ Existe |
| Application | `*/Application/` | ✅ Existe |
| Infrastructure | `*/Infrastructure/` | ✅ Existe |
### Evidencia: Estructura de Carpetas
```
Shared/
├── Domain/
│ └── Contracts/ # 23 interfaces
├── Application/
│ └── UseCases/ # Casos de uso
└── Infrastructure/
└── Services/ # Implementaciones
```
### Evidencia: Contratos en Domain (23 interfaces)
```
Shared/Domain/Contracts/
├── AjaxControllerInterface.php
├── CSSGeneratorInterface.php
├── ComponentRepositoryInterface.php
├── RendererInterface.php
├── SchemaSyncServiceInterface.php
└── ... (18 más)
```
**VEREDICTO:** ✅ Clean Architecture implementada correctamente
---
## 2. Nomenclatura
### Convención: Carpetas en PascalCase
| Módulo | Carpeta Public | Carpeta Admin | Estado |
|--------|---------------|---------------|--------|
| ContactForm | `Public/ContactForm/` | `Admin/ContactForm/` | ✅ |
| FeaturedImage | `Public/FeaturedImage/` | `Admin/FeaturedImage/` | ✅ |
| Footer | `Public/Footer/` | `Admin/Footer/` | ✅ |
| TopNotificationBar | `Public/TopNotificationBar/` | `Admin/TopNotificationBar/` | ✅ |
**Módulos verificados:** 17 en Public/, 17 en Admin/
### Convención: Schemas en kebab-case
| Schema | Nombre Archivo | component_name | Estado |
|--------|---------------|----------------|--------|
| Contact Form | `contact-form.json` | `"contact-form"` | ✅ |
| Featured Image | `featured-image.json` | `"featured-image"` | ✅ |
| Top Notification Bar | `top-notification-bar.json` | `"top-notification-bar"` | ✅ |
| CTA Box Sidebar | `cta-box-sidebar.json` | `"cta-box-sidebar"` | ✅ |
**Schemas verificados:** 17 archivos JSON
### Convención: Clases en PascalCase
| Tipo | Patrón | Ejemplo Real | Estado |
|------|--------|--------------|--------|
| Renderer | `[Component]Renderer` | `ContactFormRenderer` | ✅ |
| FormBuilder | `[Component]FormBuilder` | `ContactFormFormBuilder` | ✅ |
| Handler | `[Component]AjaxHandler` | `NewsletterAjaxHandler` | ✅ |
### Convención: Namespaces
**Patrón:** `ROITheme\[Context]\[Component]\[Layer]`
**Evidencia:**
```php
// ContactFormRenderer.php
namespace ROITheme\Public\ContactForm\Infrastructure\Ui;
// ContactFormFormBuilder.php
namespace ROITheme\Admin\ContactForm\Infrastructure\Ui;
// NewsletterAjaxHandler.php
namespace ROITheme\Public\Footer\Infrastructure\Api\WordPress;
```
**VEREDICTO:** ✅ Nomenclatura consistente en todo el tema
---
## 3. Estándares de Código PHP
### strict_types
**Comando verificación:** `grep "declare(strict_types=1)" Public/ -r`
**Resultado:** 43 archivos con strict_types
| Ubicación | Archivos con strict_types | Total |
|-----------|--------------------------|-------|
| Public/ | 43 | 43 |
| Admin/ (muestra) | ✅ Verificado | - |
| Shared/ (muestra) | ✅ Verificado | - |
### Clases final
**Resultado:** 39 archivos usan `final class`
**Evidencia:**
```php
// ContactFormRenderer.php:24
final class ContactFormRenderer implements RendererInterface
// ContactFormFormBuilder.php:19
final class ContactFormFormBuilder
// NewsletterAjaxHandler.php
final class NewsletterAjaxHandler
```
### Inyección de Dependencias via Constructor
**Patrón esperado:** Interfaces inyectadas, no clases concretas
**Evidencia:**
```php
// ContactFormRenderer.php:28-30
public function __construct(
private CSSGeneratorInterface $cssGenerator // ✅ Interface
) {}
// ContactFormFormBuilder.php:21-23
public function __construct(
private AdminDashboardRenderer $renderer // ✅ DI via constructor
) {}
```
**VEREDICTO:** ✅ Estándares PHP cumplidos
---
## 4. Patrones de Renderer
### Patrón: supports() retorna kebab-case
**Evidencia ContactFormRenderer.php:71-74:**
```php
private const COMPONENT_NAME = 'contact-form'; // ✅ kebab-case
public function supports(string $componentType): bool
{
return $componentType === self::COMPONENT_NAME; // ✅
}
```
### Patrón: Validación de Visibilidad (3 campos)
**Evidencia ContactFormRenderer.php:76-99:**
```php
private function isEnabled(array $data): bool
{
$value = $data['visibility']['is_enabled'] ?? false; // ✅
return $value === true || $value === '1' || $value === 1;
}
private function getVisibilityClass(array $data): ?string
{
$showDesktop = $data['visibility']['show_on_desktop'] ?? true; // ✅
$showMobile = $data['visibility']['show_on_mobile'] ?? true; // ✅
// ...
}
```
### Patrón: CSS via Generator (no hardcodeado)
**Evidencia ContactFormRenderer.php:49:**
```php
$css = $this->generateCSS($data); // ✅ Usa CSSGenerator inyectado
```
**VEREDICTO:** ✅ Patrones de Renderer cumplidos
---
## 5. Patrones de FormBuilder
### Patrón: Design System Consistente
**Colores esperados:** `#0E2337`, `#1e3a5f`, `#FF8600`
**Evidencia ContactFormFormBuilder.php:57-58:**
```php
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
```
### Patrón: data-component en kebab-case
**Evidencia ContactFormFormBuilder.php:69:**
```php
$html .= '... data-component="contact-form">'; // ✅ kebab-case
```
### Patrón: Bootstrap 5
**Evidencia ContactFormFormBuilder.php:31:**
```php
$html .= '<div class="row g-3">'; // ✅ Bootstrap 5 grid
```
**VEREDICTO:** ✅ Patrones de FormBuilder cumplidos
---
## 6. Patrones de Schema JSON
### Campos Obligatorios de Visibilidad
**Evidencia contact-form.json:6-31:**
```json
"visibility": {
"label": "Visibilidad",
"priority": 10,
"fields": {
"is_enabled": { ... }, // ✅ Obligatorio
"show_on_desktop": { ... }, // ✅ Obligatorio
"show_on_mobile": { ... } // ✅ Obligatorio
}
}
```
### Estructura de Grupos
**Evidencia contact-form.json:**
```json
{
"component_name": "contact-form", // ✅ kebab-case
"version": "1.0.0", // ✅ Versionado
"description": "...", // ✅ Documentado
"groups": {
"visibility": { "priority": 10 },
"content": { "priority": 20 },
// ...
}
}
```
**VEREDICTO:** ✅ Patrones de Schema cumplidos
---
## 7. Estructura OpenSpec Actual
### Archivos Existentes
```
openspec/
├── AGENTS.md ✅ (456 líneas)
├── project.md ✅ (contiene convenciones)
├── specs/
│ ├── 00arquitectura-limpia/spec.md ✅ (215 líneas)
│ ├── 00estandares-codigo/spec.md ✅ (350 líneas)
│ └── ...
└── changes/ ✅ (registro de cambios)
```
### Archivos Faltantes (para mejorar)
| Archivo | Estado | Acción |
|---------|--------|--------|
| WORKFLOW-ROI-THEME.md | ❌ No existe | CREAR |
| nomenclatura/spec.md | ❌ No existe | CREAR |
**NOTA:** Los archivos faltantes son DOCUMENTACIÓN de patrones que YA existen en el código.
---
## 8. Tabla Resumen de Validación
| Especificación | Código Actual | Estado |
|----------------|---------------|--------|
| Clean Architecture (3 capas) | Shared/Domain, /Application, /Infrastructure | ✅ CUMPLE |
| Carpetas PascalCase | ContactForm/, FeaturedImage/, etc. | ✅ CUMPLE |
| Schemas kebab-case | contact-form.json, featured-image.json | ✅ CUMPLE |
| component_name kebab-case | "contact-form", "featured-image" | ✅ CUMPLE |
| strict_types=1 | 43+ archivos verificados | ✅ CUMPLE |
| final class | 39+ clases verificadas | ✅ CUMPLE |
| DI via constructor | CSSGeneratorInterface, etc. | ✅ CUMPLE |
| supports() kebab-case | return 'contact-form' | ✅ CUMPLE |
| 3 campos visibilidad | is_enabled, show_on_desktop, show_on_mobile | ✅ CUMPLE |
| Design System colores | #0E2337, #1e3a5f, #FF8600 | ✅ CUMPLE |
| Namespaces ROITheme\... | ROITheme\Public\ContactForm\... | ✅ CUMPLE |
---
## 9. Conclusión
### El código existente YA implementa:
1. **Clean Architecture** - Capas Domain/Application/Infrastructure
2. **Convenciones de Nomenclatura** - PascalCase, kebab-case según contexto
3. **Estándares PHP** - strict_types, final class, DI
4. **Patrones de Componentes** - Renderer, FormBuilder, Schema
5. **Design System** - Colores consistentes
### Las especificaciones mejoradas serán:
1. **DOCUMENTACIÓN** de patrones existentes (no invenciones)
2. **CLARIFICACIÓN** de reglas implícitas
3. **EJEMPLOS** de código correcto vs incorrecto
4. **WORKFLOW** formalizado (que ya se sigue informalmente)
### Beneficio de las mejoras:
- Mayor claridad para nuevos desarrolladores
- Referencia rápida de patrones
- Validación automatizable
- Onboarding más rápido
---
**Última actualización:** 2026-01-08

View File

@@ -1,215 +0,0 @@
# Especificacion de Arquitectura Limpia
## Purpose
Define la implementacion de Clean Architecture para ROITheme, un tema WordPress que sigue principios de Domain-Driven Design con separacion fisica de contextos delimitados (Admin, Public, Shared).
## Requirements
### Requirement: Separacion Fisica de Contextos
The system MUST organize code into three physically delimited contexts: Admin/, Public/, and Shared/.
#### Scenario: Codigo pertenece al contexto de administracion
- **WHEN** el codigo maneja operaciones CRUD, configuracion o funcionalidad del panel admin
- **THEN** el codigo DEBE colocarse en el directorio `Admin/`
- **AND** el codigo NO DEBE importar del directorio `Public/`
#### Scenario: Codigo pertenece al contexto publico/frontend
- **WHEN** el codigo maneja renderizado, visualizacion o presentacion frontend
- **THEN** el codigo DEBE colocarse en el directorio `Public/`
- **AND** el codigo NO DEBE importar del directorio `Admin/`
#### Scenario: Codigo es compartido entre contextos
- **WHEN** el codigo es usado por AMBOS contextos Admin/ y Public/
- **THEN** el codigo DEBE colocarse en el directorio `Shared/` raiz
- **AND** tanto Admin/ como Public/ PUEDEN importar de Shared/
---
### Requirement: Organizacion Granular de Codigo Compartido
The system MUST implement three levels of shared code to avoid mixing context-specific shared code.
#### Scenario: Codigo compartido solo dentro del contexto Admin
- **WHEN** el codigo es reutilizado por multiples modulos Admin pero NO por Public
- **THEN** el codigo DEBE colocarse en el directorio `Admin/Shared/`
- **AND** los modulos de Public/ NO DEBEN importar de `Admin/Shared/`
#### Scenario: Codigo compartido solo dentro del contexto Public
- **WHEN** el codigo es reutilizado por multiples modulos Public pero NO por Admin
- **THEN** el codigo DEBE colocarse en el directorio `Public/Shared/`
- **AND** los modulos de Admin/ NO DEBEN importar de `Public/Shared/`
#### Scenario: Codigo compartido entre ambos contextos
- **WHEN** el codigo es reutilizado por AMBOS modulos Admin/ y Public/
- **THEN** el codigo DEBE colocarse en el directorio `Shared/` raiz
- **AND** esto incluye ValueObjects, Exceptions y Contracts base
---
### Requirement: Cada Contexto Sigue las Capas de Clean Architecture
Each context (Admin/, Public/, Shared/) MUST internally implement the three Clean Architecture layers: Domain, Application, Infrastructure.
#### Scenario: Estructura de modulo dentro del contexto Admin
- **GIVEN** un componente llamado "Navbar" en el contexto Admin
- **WHEN** el modulo es creado
- **THEN** la estructura DEBE ser: Admin/Navbar/ con subcarpetas Domain/, Application/, Infrastructure/
#### Scenario: Estructura de modulo dentro del contexto Public
- **GIVEN** un componente llamado "Navbar" en el contexto Public
- **WHEN** el modulo es creado
- **THEN** la estructura DEBE ser: Public/Navbar/ con subcarpetas Domain/, Application/, Infrastructure/
---
### Requirement: Cumplimiento de Direccion de Dependencias
The system MUST enforce that dependencies flow ONLY from outer layers to inner layers.
#### Scenario: Infrastructure depende de Application y Domain
- **WHEN** el codigo esta en la capa Infrastructure
- **THEN** PUEDE importar de la capa Application
- **AND** PUEDE importar de la capa Domain
#### Scenario: Application depende solo de Domain
- **WHEN** el codigo esta en la capa Application
- **THEN** PUEDE importar de la capa Domain
- **AND** NO DEBE importar de la capa Infrastructure
#### Scenario: Domain no tiene dependencias externas
- **WHEN** el codigo esta en la capa Domain
- **THEN** NO DEBE importar de la capa Application
- **AND** NO DEBE importar de la capa Infrastructure
- **AND** NO DEBE importar funciones o globales de WordPress
---
### Requirement: La Capa Domain Contiene Solo Logica de Negocio Pura
The Domain layer MUST contain only pure business logic without framework dependencies.
#### Scenario: Validacion de contenido de capa Domain
- **WHEN** el codigo se coloca en la capa Domain
- **THEN** PUEDE contener Entities, Value Objects, Domain Services, Interfaces, Exceptions
- **AND** NO DEBE contener global $wpdb, $_POST, $_GET, $_SESSION, add_action, add_filter, HTML, CSS, JavaScript
#### Scenario: Implementacion de entidad Domain
- **GIVEN** una entidad Domain como NavbarConfiguration
- **WHEN** la entidad es implementada
- **THEN** DEBE contener reglas de negocio y validacion
- **AND** NO DEBE contener logica de persistencia
- **AND** NO DEBE referenciar APIs de WordPress
---
### Requirement: La Capa Application Orquesta Domain
The Application layer MUST orchestrate domain entities without containing business logic.
#### Scenario: Implementacion de Use Case
- **WHEN** un Use Case es implementado
- **THEN** DEBE coordinar entidades y servicios de domain
- **AND** DEBE depender de interfaces, NO de implementaciones concretas
- **AND** NO DEBE contener reglas de validacion de negocio
#### Scenario: Uso de DTOs para transferencia de datos
- **WHEN** los datos cruzan limites entre capas
- **THEN** se DEBEN usar DTOs (Data Transfer Objects)
- **AND** los DTOs DEBEN ser contenedores de datos simples sin logica de negocio
---
### Requirement: Infrastructure Implementa Interfaces
The Infrastructure layer MUST implement interfaces defined in Domain/Application layers.
#### Scenario: Implementacion de Repository
- **GIVEN** una RepositoryInterface definida en Domain
- **WHEN** el repository es implementado
- **THEN** DEBE colocarse en Infrastructure/Persistence/
- **AND** DEBE implementar la interface de Domain
- **AND** PUEDE usar global $wpdb o APIs de WordPress
#### Scenario: Integracion con WordPress
- **WHEN** se necesita codigo especifico de WordPress
- **THEN** DEBE colocarse en la capa Infrastructure
- **AND** NO DEBE filtrarse a las capas Domain o Application
---
### Requirement: Los Modulos Son Autocontenidos e Independientes
Each module (Navbar, Footer, Toolbar, etc.) MUST be self-contained and independent from other modules.
#### Scenario: Aislamiento de modulos
- **WHEN** un modulo como Admin/Navbar/ es implementado
- **THEN** NO DEBE importar de Admin/Footer/
- **AND** NO DEBE importar de Admin/Toolbar/
- **AND** SOLO PUEDE importar de Shared/
#### Scenario: Eliminacion de modulos
- **WHEN** un modulo necesita ser eliminado
- **THEN** borrar la carpeta del modulo NO DEBE romper otros modulos
- **AND** no se DEBERIAN requerir cambios de codigo en otros modulos
---
### Requirement: Admin y Public Son Bounded Contexts Separados
Admin/ and Public/ MUST be treated as separate bounded contexts because they have different responsibilities.
#### Scenario: Responsabilidad del contexto Admin
- **WHEN** el codigo maneja administracion de componentes
- **THEN** la entidad Domain se enfoca en configuracion, validacion, estados draft/published
- **AND** los Use Cases se enfocan en operaciones Save, Update, Delete, Get
#### Scenario: Responsabilidad del contexto Public
- **WHEN** el codigo maneja renderizado de componentes
- **THEN** la entidad Domain se enfoca en estado activo, caching, filtrado por permisos
- **AND** los Use Cases se enfocan en operaciones GetActive, Render, Cache
#### Scenario: No hay duplicacion de domain
- **WHEN** Admin/Navbar/Domain/ y Public/Navbar/Domain/ ambos existen
- **THEN** NO son duplicados sino bounded contexts especializados
- **AND** Admin se enfoca en configuracion/gestion
- **AND** Public se enfoca en renderizado/visualizacion
---
### Requirement: Validacion de Arquitectura Antes de Commit
The system MUST validate architectural compliance before committing code.
#### Scenario: Validacion de capa Domain
- **WHEN** se valida codigo de la capa Domain
- **THEN** grep por global $wpdb DEBE retornar vacio
- **AND** grep por add_action DEBE retornar vacio
- **AND** grep por $_POST DEBE retornar vacio
#### Scenario: Validacion de dependencias de modulos
- **WHEN** se validan dependencias entre modulos
- **THEN** imports de Admin/Navbar/ desde Admin/Footer/ NO DEBEN existir
- **AND** imports de Public/Navbar/ desde Public/Footer/ NO DEBEN existir
---
### Requirement: Realizacion de Beneficios de la Arquitectura
The architecture MUST provide measurable benefits.
#### Scenario: Asignacion granular de trabajo
- **WHEN** un desarrollador es asignado a trabajar en Admin/Navbar/
- **THEN** puede acceder SOLO a esa carpeta
- **AND** no puede ver ni modificar Public/ u otros modulos de Admin/
#### Scenario: Eliminacion facil de modulos
- **WHEN** un componente ya no es necesario
- **THEN** eliminarlo requiere solo borrar la carpeta
- **AND** no se necesitan otras modificaciones de codigo
#### Scenario: Codigo compartido consistente
- **WHEN** se encuentra un bug en un ValueObject compartido
- **THEN** arreglarlo en Shared/Domain/ValueObjects/ lo arregla para TODOS los modulos
- **AND** no se necesita actualizar codigo duplicado

View File

@@ -1,350 +0,0 @@
# Especificacion de Estandares de Codigo
## Purpose
Define los principios SOLID, estandares de POO (Programacion Orientada a Objetos) y estandares generales de codigo que DEBEN seguirse en el desarrollo de ROITheme.
## Requirements
### Requirement: Principio de Responsabilidad Unica (SRP)
Each class MUST have exactly one reason to change and one responsibility.
#### Scenario: Responsabilidad de clase Use Case
- **WHEN** se crea una clase Use Case
- **THEN** DEBE manejar exactamente UNA operacion (Save, Get, Delete, etc.)
- **AND** NO DEBE combinar multiples operaciones en una clase
#### Scenario: Validacion de tamano de clase
- **WHEN** se crea un archivo de clase
- **THEN** DEBERIA tener menos de 300 lineas
- **AND** DEBERIA tener maximo 3-5 metodos privados
- **AND** el nombre de la clase DEBE describir su unica responsabilidad
#### Scenario: Violacion de SRP
- **WHEN** una clase contiene save(), get(), delete(), validate(), sendEmail()
- **THEN** DEBE dividirse en clases Use Case separadas
- **AND** cada clase maneja solo una operacion
---
### Requirement: Principio Abierto/Cerrado (OCP)
Classes MUST be open for extension but closed for modification.
#### Scenario: Agregar nuevo tipo de componente
- **WHEN** se necesita un nuevo tipo de componente
- **THEN** se DEBE crear una nueva subclase
- **AND** la clase BaseComponent existente NO DEBE modificarse
- **AND** NO se DEBERIAN agregar cadenas if/elseif para nuevos tipos
#### Scenario: Extender funcionalidad base
- **GIVEN** que existe una clase abstracta BaseComponent
- **WHEN** se necesita comportamiento especializado
- **THEN** se DEBE usar herencia para extender
- **AND** la clase base DEBE permanecer sin cambios
---
### Requirement: Principio de Sustitucion de Liskov (LSP)
Subclasses MUST be substitutable for their base classes without breaking functionality.
#### Scenario: Uso polimorfico
- **GIVEN** una funcion que acepta parametro BaseComponent
- **WHEN** cualquier subclase es pasada
- **THEN** la funcion DEBE funcionar correctamente
- **AND** NO se DEBERIAN lanzar excepciones inesperadas
#### Scenario: Cumplimiento de contrato
- **WHEN** una subclase sobrescribe un metodo padre
- **THEN** DEBE respetar el contrato del metodo original
- **AND** las precondiciones NO DEBEN ser mas restrictivas
- **AND** las postcondiciones NO DEBEN ser mas permisivas
---
### Requirement: Principio de Segregacion de Interfaces (ISP)
Interfaces MUST be small and specific, not large and general.
#### Scenario: Validacion de tamano de interface
- **WHEN** se define una interface
- **THEN** DEBE tener maximo 3-5 metodos
- **AND** cada metodo DEBE relacionarse con la misma capacidad
#### Scenario: Evitar interfaces gordas
- **WHEN** existen multiples capacidades no relacionadas
- **THEN** se DEBEN crear interfaces separadas
- **AND** las clases implementan solo las interfaces que usan
- **AND** NO se permiten metodos dummy "No implementado"
#### Scenario: Diseno correcto de interface
- **WHEN** se necesita funcionalidad de cache
- **THEN** se DEBE usar CacheInterface con get(), set(), delete()
- **AND** ValidatorInterface con validate() es separada
---
### Requirement: Principio de Inversion de Dependencias (DIP)
High-level modules MUST depend on abstractions, not concrete implementations.
#### Scenario: Inyeccion por constructor con interfaces
- **WHEN** una clase necesita dependencias
- **THEN** el constructor DEBE recibir interfaces, NO clases concretas
- **AND** NO debe haber new ClaseConcreta() dentro del cuerpo de la clase
#### Scenario: Cableado del Contenedor DI
- **WHEN** se necesitan implementaciones concretas
- **THEN** el DIContainer DEBE manejar el cableado
- **AND** las clases permanecen desacopladas de las implementaciones
#### Scenario: Dependencia incorrecta
- **WHEN** el constructor hace this->repo = new WordPressNavbarRepository()
- **THEN** esto DEBE refactorizarse para recibir NavbarRepositoryInterface
- **AND** el DIContainer proporciona la implementacion concreta
---
### Requirement: Encapsulacion de Propiedades
Class properties MUST be encapsulated with controlled access.
#### Scenario: Visibilidad de propiedades
- **WHEN** se define una propiedad de clase
- **THEN** DEBE ser private o protected
- **AND** el acceso DEBE ser via metodos getter
- **AND** la mutacion DEBE ser via metodos setter o metodos de negocio
#### Scenario: Encapsulacion de Value Object
- **GIVEN** un ValueObject como ComponentName
- **WHEN** es construido
- **THEN** la validacion DEBE ocurrir en el constructor
- **AND** el valor DEBE ser inmutable despues de la construccion
- **AND** los detalles internos NO DEBEN exponerse
---
### Requirement: Guias de Herencia
Inheritance MUST be used appropriately with limited depth.
#### Scenario: Limite de profundidad de herencia
- **WHEN** se usa herencia
- **THEN** la profundidad maxima DEBE ser 2-3 niveles
- **AND** las cadenas de herencia profundas DEBEN evitarse
#### Scenario: Comportamiento comun en clase base
- **WHEN** multiples clases comparten comportamiento comun
- **THEN** se DEBERIA crear una clase base abstracta
- **AND** las subclases especializan con comportamiento adicional
---
### Requirement: Polimorfismo Correcto
Methods MUST accept base types or interfaces to enable polymorphism.
#### Scenario: Tipos de parametros de metodo
- **WHEN** un metodo acepta parametro de componente
- **THEN** el type hint DEBERIA ser BaseComponent o ComponentInterface
- **AND** cualquier subclase/implementacion DEBE funcionar correctamente
#### Scenario: Polimorfismo de repository
- **WHEN** un Use Case usa un repository
- **THEN** DEBE aceptar RepositoryInterface
- **AND** WordPressRepository y MockRepository funcionan transparentemente
---
### Requirement: Estandares PHP Estrictos
All PHP code MUST follow strict type safety and naming conventions.
#### Scenario: Declaracion de tipos estrictos
- **WHEN** se crea un archivo PHP
- **THEN** DEBE comenzar con declare(strict_types=1)
- **AND** los tipos de retorno DEBEN declararse
- **AND** los tipos de parametros DEBEN declararse
#### Scenario: Convencion de namespace
- **WHEN** se crea una clase
- **THEN** el namespace DEBE seguir ROITheme\[Contexto]\[Componente]\[Capa]
- **AND** DEBE soportar autoloading PSR-4
#### Scenario: Declaracion de clase
- **WHEN** se crea una clase
- **THEN** DEBERIA ser final por defecto
- **AND** solo hacerla no-final cuando se pretende herencia
- **AND** el nombre de clase DEBE ser PascalCase
---
### Requirement: Modularidad del Codigo
Code MUST be organized into independent and cohesive modules.
#### Scenario: Independencia de modulos
- **WHEN** se crea un modulo
- **THEN** DEBE ser autocontenido
- **AND** NO DEBE depender de otros modulos (solo de Shared/)
- **AND** eliminarlo NO DEBE romper otros modulos
#### Scenario: Alta cohesion
- **WHEN** el codigo se coloca en un modulo
- **THEN** todo el codigo DEBE relacionarse con el proposito de ese modulo
- **AND** el codigo no relacionado DEBE estar en Shared/ u otro modulo
#### Scenario: Bajo acoplamiento
- **WHEN** los modulos interactuan
- **THEN** DEBEN comunicarse a traves de interfaces de Shared/
- **AND** las dependencias directas entre modulos estan prohibidas
---
### Requirement: DRY - No Te Repitas
Code duplication MUST be eliminated through appropriate abstraction.
#### Scenario: Ubicacion de codigo compartido
- **WHEN** el codigo es usado por multiples modulos
- **THEN** DEBE moverse al nivel apropiado de Shared/
- **AND** los modulos DEBEN importar de Shared/
#### Scenario: Deteccion de duplicacion
- **WHEN** existe codigo similar en 2+ lugares
- **THEN** DEBE refactorizarse a Shared/
- **AND** las ubicaciones originales importan de Shared/
---
### Requirement: KISS - Mantenlo Simple
Solutions MUST be simple and avoid over-engineering.
#### Scenario: Uso de patrones
- **WHEN** se considera un patron de diseno
- **THEN** DEBE resolver un problema real
- **AND** se DEBEN preferir soluciones mas simples
- **AND** la abstraccion excesiva DEBE evitarse
#### Scenario: Claridad del codigo
- **WHEN** se escribe codigo
- **THEN** DEBERIA ser auto-documentado
- **AND** los comentarios DEBERIAN ser innecesarios para entender
- **AND** la logica compleja DEBERIA extraerse a metodos bien nombrados
---
### Requirement: Separacion de Responsabilidades por Capa
Each layer MUST have distinct responsibilities.
#### Scenario: Responsabilidades por capa
- **WHEN** se escribe codigo
- **THEN** Domain contiene logica de negocio
- **AND** Application contiene orquestacion
- **AND** Infrastructure contiene implementacion tecnica
- **AND** UI contiene solo presentacion
#### Scenario: Validacion de responsabilidades cruzadas
- **WHEN** se valida ubicacion de codigo
- **THEN** SQL NO DEBE estar en Domain/Application
- **AND** HTML NO DEBE estar en Domain/Application
- **AND** logica de negocio NO DEBE estar en Infrastructure
---
### Requirement: Limites de Tamano de Archivo
Files MUST be kept small and focused.
#### Scenario: Tamano de archivo de clase
- **WHEN** se crea un archivo de clase
- **THEN** DEBERIA tener menos de 300 lineas
- **AND** si es mas grande, DEBERIA dividirse en clases mas pequenas
#### Scenario: Tamano de metodo
- **WHEN** se escribe un metodo
- **THEN** DEBERIA tener menos de 30 lineas
- **AND** metodos complejos DEBERIAN extraerse a metodos auxiliares
---
### Requirement: Convenciones de Nomenclatura
Names MUST be clear, descriptive, and follow conventions.
#### Scenario: Nomenclatura de clases
- **WHEN** se nombra una clase
- **THEN** el nombre DEBE describir su unica responsabilidad
- **AND** las clases Use Case DEBEN nombrarse [Accion][Entidad]UseCase
- **AND** las clases Repository DEBEN nombrarse [Implementacion][Entidad]Repository
#### Scenario: Nomenclatura de metodos
- **WHEN** se nombra un metodo
- **THEN** DEBE describir lo que hace el metodo
- **AND** DEBERIA comenzar con un verbo
- **AND** metodos booleanos DEBERIAN comenzar con is/has/can
#### Scenario: Nomenclatura de variables
- **WHEN** se nombra una variable
- **THEN** DEBE ser descriptiva
- **AND** las abreviaturas DEBEN evitarse
- **AND** nombres de una letra solo para contadores de bucle
---
### Requirement: Validacion Pre-Commit
Code MUST pass validation before commit.
#### Scenario: Verificacion de cumplimiento SOLID
- **WHEN** el codigo esta listo para commit
- **THEN** SRP cada clase tiene una responsabilidad
- **AND** OCP nuevas caracteristicas via extension, no modificacion
- **AND** LSP las subclases son sustituibles
- **AND** ISP las interfaces son pequenas 3-5 metodos
- **AND** DIP el constructor recibe interfaces
#### Scenario: Verificacion de cumplimiento POO
- **WHEN** el codigo esta listo para commit
- **THEN** las propiedades son private/protected
- **AND** la profundidad de herencia es max 2-3 niveles
- **AND** el polimorfismo esta implementado correctamente
- **AND** la abstraccion oculta complejidad
#### Scenario: Verificacion de calidad
- **WHEN** el codigo esta listo para commit
- **THEN** los archivos tienen menos de 300 lineas
- **AND** los nombres son claros y descriptivos
- **AND** no existe duplicacion de codigo
- **AND** no hay sobre-ingenieria presente
---
### Requirement: Escaping Obligatorio en Output HTML
All HTML output MUST use WordPress escaping functions for security.
#### Scenario: Escaping de textos
- **WHEN** se genera output de texto en HTML
- **THEN** DEBE usar esc_html() para contenido de texto
#### Scenario: Escaping de atributos
- **WHEN** se genera un atributo HTML
- **THEN** DEBE usar esc_attr() para valores de atributos
#### Scenario: Escaping de URLs
- **WHEN** se genera una URL en href o src
- **THEN** DEBE usar esc_url() para URLs
#### Scenario: Escaping de textareas
- **WHEN** se genera contenido para textarea
- **THEN** DEBE usar esc_textarea() para el valor
#### Scenario: Prohibicion de output sin escaping
- **WHEN** se revisa codigo de Renderer o FormBuilder
- **THEN** NO DEBE existir echo o print de variables sin escaping
- **AND** NO DEBE existir interpolacion directa de variables en HTML

View File

@@ -1,602 +0,0 @@
# Plan de Pruebas: AdSense JavaScript-First Architecture
> **NOTA IMPORTANTE - PROTOCOLO DE PRUEBAS**
>
> Las pruebas se ejecutan en el servidor de PRODUCCION.
> Si hay algo que corregir, se modifica en LOCAL y luego se despliega.
>
> **PROHIBIDO**: Modificar codigo directamente en produccion.
> **PERMITIDO**: Solo ejecutar pruebas y verificaciones en produccion.
>
> Flujo correcto:
> 1. Ejecutar prueba en produccion
> 2. Si falla, corregir en local
> 3. Desplegar cambios a produccion
> 4. Re-ejecutar prueba
---
## Informacion del Servidor de Produccion
| Campo | Valor |
|-------|-------|
| **Host SSH** | `VPSContabo` (alias en ~/.ssh/config) |
| **IP** | `5.189.136.96` |
| **Usuario** | `root` |
| **Ruta del tema** | `/var/www/preciosunitarios/public_html/wp-content/themes/roi-theme` |
| **URL produccion** | `https://analisisdepreciosunitarios.com` |
| **PHP Version** | 8.2 (php8.2-fpm) |
### Comandos de Deploy
```bash
# 1. Push desde local (sube a GitHub + Gitea automaticamente)
git push origin main
# 2. Pull en produccion
ssh VPSContabo "cd /var/www/preciosunitarios/public_html/wp-content/themes/roi-theme && git pull origin main"
# 3. Limpiar cache de OPcache (IMPORTANTE despues de deploy)
ssh VPSContabo "systemctl restart php8.2-fpm"
```
---
## Resumen de Pruebas
| ID | Categoria | Descripcion | Criterio de Aceptacion |
|----|-----------|-------------|------------------------|
| T01 | Endpoint REST | Endpoint registrado y accesible | HTTP 200 con JSON valido |
| T02 | Endpoint REST | Headers anti-cache presentes | Cache-Control, Pragma, Expires |
| T03 | Endpoint REST | Parametro post_id requerido | HTTP 400 sin post_id |
| T04 | Endpoint REST | post_id=0 valido (archivos/home) | HTTP 200 con post_id=0 |
| T05 | Visibilidad | Componente deshabilitado | show_ads=false, reason=component_disabled |
| T06 | Visibilidad | Usuario anonimo sin exclusiones | show_ads=true, reasons=[] |
| T07 | Visibilidad | Usuario logueado excluido | show_ads=false, reason=logged_in_excluded |
| T08 | Visibilidad | Rol excluido | show_ads=false, reason=role_excluded |
| T09 | Visibilidad | Post excluido | show_ads=false, reason=post_excluded |
| T10 | JavaScript | Script cargado en frontend | roiAdsenseConfig definido |
| T11 | JavaScript | Cache localStorage funciona | Datos guardados correctamente |
| T12 | JavaScript | Fallback cuando error | Ads se muestran en error |
| T13 | Feature Flag | Modo deshabilitado = legacy | No llama endpoint |
| T14 | Feature Flag | Modo habilitado = JS-First | Llama endpoint |
| T15 | Clean Arch | Value Objects inmutables | No WordPress en Domain |
| T16 | Clean Arch | Interface en Domain | AdsenseVisibilityCheckerInterface existe |
---
## Pruebas Detalladas
### T01: Endpoint REST Registrado y Accesible
**Categoria**: Endpoint REST
**Prioridad**: CRITICA
**Spec Reference**: Requirement: Endpoint REST Visibility
**Pasos**:
1. Abrir navegador o usar curl
2. Acceder a: `https://analisisdepreciosunitarios.com/wp-json/roi-theme/v1/adsense-placement/visibility?post_id=1`
**Resultado Esperado**:
- HTTP Status: 200
- Content-Type: application/json
- Body contiene: `show_ads`, `reasons`, `cache_seconds`, `timestamp`
**Comando de Prueba**:
```bash
curl -i "https://analisisdepreciosunitarios.com/wp-json/roi-theme/v1/adsense-placement/visibility?post_id=1"
```
**Estado**: [ ] Pendiente
**Resultado**:
**Notas**:
---
### T02: Headers Anti-Cache Presentes
**Categoria**: Endpoint REST
**Prioridad**: ALTA
**Spec Reference**: Scenario: Headers anti-cache obligatorios
**Pasos**:
1. Hacer request al endpoint
2. Verificar headers de respuesta
**Resultado Esperado**:
- `Cache-Control: no-store, no-cache, must-revalidate, max-age=0`
- `Pragma: no-cache`
- `Expires: Thu, 01 Jan 1970 00:00:00 GMT` o `0`
- `Vary: Cookie`
**Comando de Prueba**:
```bash
curl -I "https://analisisdepreciosunitarios.com/wp-json/roi-theme/v1/adsense-placement/visibility?post_id=1"
```
**Estado**: [ ] Pendiente
**Resultado**:
**Notas**:
---
### T03: Parametro post_id Requerido
**Categoria**: Endpoint REST
**Prioridad**: ALTA
**Spec Reference**: Scenario: Parametros del endpoint
**Pasos**:
1. Hacer request SIN post_id
2. Verificar respuesta de error
**Resultado Esperado**:
- HTTP Status: 400 (Bad Request)
- Body contiene mensaje de error indicando que post_id es requerido
**Comando de Prueba**:
```bash
curl -i "https://analisisdepreciosunitarios.com/wp-json/roi-theme/v1/adsense-placement/visibility"
```
**Estado**: [ ] Pendiente
**Resultado**:
**Notas**:
---
### T04: post_id=0 Valido para Paginas de Archivo
**Categoria**: Endpoint REST
**Prioridad**: ALTA
**Spec Reference**: Scenario: Parametros del endpoint (validate_callback >= 0)
**Pasos**:
1. Hacer request con post_id=0
2. Verificar que responde correctamente
**Resultado Esperado**:
- HTTP Status: 200
- Body contiene decision de visibilidad valida
**Comando de Prueba**:
```bash
curl -i "https://analisisdepreciosunitarios.com/wp-json/roi-theme/v1/adsense-placement/visibility?post_id=0"
```
**Estado**: [ ] Pendiente
**Resultado**:
**Notas**:
---
### T05: Componente Deshabilitado Retorna False
**Categoria**: Visibilidad
**Prioridad**: ALTA
**Spec Reference**: Scenario: Componente deshabilitado
**Pre-condicion**:
- Deshabilitar componente en admin (is_enabled = false)
**Pasos**:
1. Deshabilitar adsense-placement en admin
2. Hacer request al endpoint
3. Verificar respuesta
**Resultado Esperado**:
```json
{
"show_ads": false,
"reasons": ["component_disabled"],
"cache_seconds": 3600
}
```
**Comando de Prueba**:
```bash
curl "https://analisisdepreciosunitarios.com/wp-json/roi-theme/v1/adsense-placement/visibility?post_id=1"
```
**Estado**: [ ] Pendiente
**Resultado**:
**Notas**:
---
### T06: Usuario Anonimo Sin Exclusiones Ve Ads
**Categoria**: Visibilidad
**Prioridad**: CRITICA
**Spec Reference**: Scenario: Usuario anonimo sin exclusiones
**Pre-condicion**:
- Componente habilitado
- javascript_first_mode habilitado
- Sin exclusiones configuradas
- No estar logueado
**Pasos**:
1. Abrir navegador en modo incognito
2. Acceder a un post del sitio
3. Verificar respuesta del endpoint
**Resultado Esperado**:
```json
{
"show_ads": true,
"reasons": [],
"cache_seconds": 60
}
```
**Comando de Prueba**:
```bash
curl "https://analisisdepreciosunitarios.com/wp-json/roi-theme/v1/adsense-placement/visibility?post_id=1"
```
**Estado**: [ ] Pendiente
**Resultado**:
**Notas**:
---
### T07: Usuario Logueado Excluido
**Categoria**: Visibilidad
**Prioridad**: ALTA
**Spec Reference**: Scenario: Usuario logueado excluido
**Pre-condicion**:
- Activar "Ocultar para usuarios logueados" en admin
**Pasos**:
1. Loguearse en WordPress
2. Copiar cookies de sesion
3. Hacer request con cookies
**Resultado Esperado**:
```json
{
"show_ads": false,
"reasons": ["logged_in_excluded"],
"cache_seconds": 300
}
```
**Verificacion Manual**:
1. Loguearse en wp-admin
2. Visitar un post en el frontend
3. Abrir DevTools > Network
4. Buscar request a `/visibility`
5. Verificar respuesta
**Estado**: [ ] Pendiente
**Resultado**:
**Notas**:
---
### T08: Rol Excluido
**Categoria**: Visibilidad
**Prioridad**: ALTA
**Spec Reference**: Scenario: Rol de usuario excluido
**Pre-condicion**:
- Agregar "administrator" a roles excluidos en admin
**Pasos**:
1. Loguearse como administrator
2. Visitar un post
3. Verificar respuesta del endpoint
**Resultado Esperado**:
```json
{
"show_ads": false,
"reasons": ["role_excluded"],
"cache_seconds": 300
}
```
**Estado**: [ ] Pendiente
**Resultado**:
**Notas**:
---
### T09: Post Excluido por ID
**Categoria**: Visibilidad
**Prioridad**: ALTA
**Spec Reference**: Scenario: Post excluido por ID
**Pre-condicion**:
- Agregar un ID de post a "IDs de posts excluidos" en admin
**Pasos**:
1. Anotar el ID del post excluido (ej: 123)
2. Hacer request con ese post_id
3. Verificar respuesta
**Resultado Esperado**:
```json
{
"show_ads": false,
"reasons": ["post_excluded"],
"cache_seconds": 60
}
```
**Comando de Prueba**:
```bash
curl "https://analisisdepreciosunitarios.com/wp-json/roi-theme/v1/adsense-placement/visibility?post_id=123"
```
**Estado**: [ ] Pendiente
**Resultado**:
**Notas**:
---
### T10: Script JavaScript Cargado
**Categoria**: JavaScript
**Prioridad**: CRITICA
**Spec Reference**: Scenario: Configuracion via wp_localize_script
**Pre-condicion**:
- javascript_first_mode habilitado
**Pasos**:
1. Visitar un post en el frontend
2. Abrir DevTools > Console
3. Escribir: `window.roiAdsenseConfig`
**Resultado Esperado**:
- Objeto definido con propiedades:
- `endpoint`: URL del endpoint REST
- `postId`: ID del post actual
- `nonce`: String no vacio
- `featureEnabled`: true
- `debug`: boolean
**Verificacion Alternativa**:
```javascript
// En consola del navegador
console.log(window.roiAdsenseConfig);
console.log(typeof window.roiAdsenseVisibility);
```
**Estado**: [ ] Pendiente
**Resultado**:
**Notas**:
---
### T11: Cache localStorage Funciona
**Categoria**: JavaScript
**Prioridad**: ALTA
**Spec Reference**: Scenario: Cache en localStorage
**Pasos**:
1. Visitar un post (primera vez)
2. Abrir DevTools > Application > Local Storage
3. Buscar key `roi_adsense_visibility`
4. Recargar pagina
5. Verificar en Network que NO hay nueva llamada al endpoint
**Resultado Esperado**:
- localStorage contiene:
```json
{
"show_ads": true,
"reasons": [],
"timestamp": 1733900000,
"cache_seconds": 60
}
```
- Segunda carga NO hace request al endpoint (usa cache)
**Verificacion en Consola**:
```javascript
localStorage.getItem('roi_adsense_visibility');
```
**Estado**: [ ] Pendiente
**Resultado**:
**Notas**:
---
### T12: Fallback en Error de Red
**Categoria**: JavaScript
**Prioridad**: ALTA
**Spec Reference**: Scenario: Fallback strategy cached-or-show
**Pasos**:
1. Limpiar localStorage
2. Abrir DevTools > Network
3. Habilitar "Offline" mode
4. Visitar un post
5. Verificar comportamiento
**Resultado Esperado**:
- Los ads se muestran (fallback = show)
- No hay error en consola (error manejado gracefully)
**Verificacion Alternativa**:
```javascript
// Limpiar cache
window.roiAdsenseVisibility.clearCache();
// Recargar con network offline
```
**Estado**: [ ] Pendiente
**Resultado**:
**Notas**:
---
### T13: Feature Flag Deshabilitado = Modo Legacy
**Categoria**: Feature Flag
**Prioridad**: ALTA
**Spec Reference**: Scenario: Feature flag deshabilitado
**Pre-condicion**:
- Deshabilitar javascript_first_mode en admin
**Pasos**:
1. Deshabilitar javascript_first_mode
2. Visitar un post
3. Verificar en Network que NO hay llamada al endpoint
**Resultado Esperado**:
- `roiAdsenseConfig.featureEnabled` = false
- No hay request a `/visibility` endpoint
- Ads se muestran inmediatamente (modo legacy)
**Estado**: [ ] Pendiente
**Resultado**:
**Notas**:
---
### T14: Feature Flag Habilitado = JS-First
**Categoria**: Feature Flag
**Prioridad**: ALTA
**Spec Reference**: Scenario: Feature flag habilitado
**Pre-condicion**:
- Habilitar javascript_first_mode en admin
**Pasos**:
1. Habilitar javascript_first_mode
2. Limpiar cache (localStorage y pagina)
3. Visitar un post
4. Verificar en Network que SI hay llamada al endpoint
**Resultado Esperado**:
- `roiAdsenseConfig.featureEnabled` = true
- Request a `/visibility` endpoint presente
- Ads se muestran/ocultan segun respuesta
**Estado**: [ ] Pendiente
**Resultado**:
**Notas**:
---
### T15: Value Objects Sin Dependencias WordPress
**Categoria**: Clean Architecture
**Prioridad**: MEDIA
**Spec Reference**: Scenario: Value Object VisibilityDecision en Domain
**Verificacion**:
Revisar que los archivos NO contengan funciones de WordPress:
**Archivos a verificar**:
- `Domain/ValueObjects/UserContext.php`
- `Domain/ValueObjects/VisibilityDecision.php`
- `Domain/ValueObjects/AdsenseSettings.php`
- `Domain/Contracts/AdsenseVisibilityCheckerInterface.php`
- `Application/UseCases/CheckAdsenseVisibilityUseCase.php`
**Resultado Esperado**:
- Sin `get_`, `wp_`, `is_user_logged_in`, `WP_*` classes
- Solo PHP puro y tipos del proyecto
**Estado**: [ ] Pendiente
**Resultado**:
**Notas**:
---
### T16: Interface en Domain
**Categoria**: Clean Architecture
**Prioridad**: MEDIA
**Spec Reference**: Scenario: Interface en Domain
**Verificacion**:
El archivo `Domain/Contracts/AdsenseVisibilityCheckerInterface.php` debe:
- Existir en la ruta correcta
- Definir metodo `check(int $postId, UserContext $userContext): VisibilityDecision`
- NO referenciar WordPress
**Estado**: [ ] Pendiente
**Resultado**:
**Notas**:
---
## Checklist de Despliegue Pre-Pruebas
Antes de ejecutar las pruebas, verificar:
- [ ] Codigo desplegado a produccion via FTP/SSH
- [ ] Cache de pagina limpiado
- [ ] javascript_first_mode habilitado en admin
- [ ] Componente adsense-placement habilitado
- [ ] Schema sincronizado en BD (campo javascript_first_mode existe)
---
## Registro de Ejecucion
| Fecha | Tester | Pruebas Ejecutadas | Pasadas | Fallidas | Notas |
|-------|--------|-------------------|---------|----------|-------|
| 2025-12-11 | Claude | T01-T04, T13, T15-T16 | 7 | 0 | javascript_first_mode deshabilitado, pruebas T05-T12,T14 requieren habilitarlo |
---
## Resultados de Pruebas (2025-12-11)
### Pruebas Ejecutadas
| ID | Resultado | Evidencia |
|----|-----------|-----------|
| T01 | ✅ PASA | HTTP 200, JSON: `{"show_ads":false,"reasons":["javascript_first_disabled"],"cache_seconds":600,"timestamp":...}` |
| T02 | ✅ PASA | Headers: `Cache-Control: no-store, no-cache, must-revalidate, max-age=0`, `pragma: no-cache`, `expires: Thu, 01 Jan 1970 00:00:00 GMT` |
| T03 | ✅ PASA | HTTP 400: `{"code":"rest_missing_callback_param","message":"Parametro(s) que falta(n): post_id"}` |
| T04 | ✅ PASA | HTTP 200 con post_id=0 |
| T13 | ✅ PASA | Modo legacy activo - roiAdsenseConfig tiene formato antiguo (lazyEnabled, rootMargin) sin endpoint/postId |
| T15 | ✅ PASA | grep en Domain/ no encuentra wp_, get_, is_user, WP_ |
| T16 | ✅ PASA | AdsenseVisibilityCheckerInterface.php existe en Domain/Contracts/ |
### Pruebas Pendientes (requieren javascript_first_mode = true)
| ID | Razon Pendiente |
|----|-----------------|
| T05 | Requiere deshabilitar componente completo |
| T06 | Requiere javascript_first_mode habilitado |
| T07 | Requiere javascript_first_mode + hide_for_logged_in |
| T08 | Requiere javascript_first_mode + roles excluidos |
| T09 | Requiere javascript_first_mode + post excluido |
| T10 | Requiere javascript_first_mode habilitado |
| T11 | Requiere javascript_first_mode habilitado |
| T12 | Requiere javascript_first_mode habilitado |
| T14 | Requiere javascript_first_mode habilitado |
---
## Defectos Encontrados
| ID | Prueba | Descripcion | Severidad | Estado | Correccion |
|----|--------|-------------|-----------|--------|------------|
| - | - | Sin defectos encontrados | - | - | - |
---
## Historial de Versiones
| Version | Fecha | Cambios |
|---------|-------|---------|
| 1.0 | 2025-12-11 | Plan inicial basado en spec v1.5 |
| 1.1 | 2025-12-11 | Agregada info servidor produccion, resultados primera ronda de pruebas |