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:
437
_openspec/WORKFLOW-ROI-THEME.md
Normal file
437
_openspec/WORKFLOW-ROI-THEME.md
Normal 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
|
||||
365
_openspec/changes/adsense-auto-ads-toggle/spec.md
Normal file
365
_openspec/changes/adsense-auto-ads-toggle/spec.md
Normal 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
|
||||
309
_openspec/changes/adsense-cache-unified-visibility/spec.md
Normal file
309
_openspec/changes/adsense-cache-unified-visibility/spec.md
Normal 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 |
|
||||
214
_openspec/changes/adsense-cache-unified-visibility/test-plan.md
Normal file
214
_openspec/changes/adsense-cache-unified-visibility/test-plan.md
Normal 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 |
|
||||
1334
_openspec/changes/adsense-javascript-first/test-plan.md
Normal file
1334
_openspec/changes/adsense-javascript-first/test-plan.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -59,7 +59,7 @@ The first step MUST be creating the component JSON schema.
|
||||
|
||||
#### Scenario: Fuente del 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
|
||||
|
||||
#### Scenario: Campos obligatorios de visibilidad
|
||||
111
_openspec/changes/recaptcha-anti-spam/proposal.md
Normal file
111
_openspec/changes/recaptcha-anti-spam/proposal.md
Normal 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
|
||||
325
_openspec/changes/recaptcha-anti-spam/spec.md
Normal file
325
_openspec/changes/recaptcha-anti-spam/spec.md
Normal 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
|
||||
132
_openspec/changes/recaptcha-anti-spam/tasks.md
Normal file
132
_openspec/changes/recaptcha-anti-spam/tasks.md
Normal 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
|
||||
@@ -66,6 +66,9 @@ El sistema maneja componentes UI configurables para un sitio de análisis de pre
|
||||
- **FormBuilder**: Panel admin para configurar valores
|
||||
|
||||
### Flujo de 5 Fases para Componentes
|
||||
|
||||
> **Nota**: `[nombre]` = kebab-case (ej: `contact-form`), `[Nombre]` = PascalCase (ej: `ContactForm`)
|
||||
|
||||
1. Schema JSON → `Schemas/[nombre].json`
|
||||
2. Sincronización → `wp roi-theme sync-component [nombre]`
|
||||
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`)
|
||||
|
||||
## Referencias Documentación
|
||||
- Arquitectura: `_planeacion/roi-theme/_arquitectura/`
|
||||
- Template HTML: `_planeacion/roi-theme/roi-theme-template/index.html`
|
||||
- Design System: `_planeacion/roi-theme/_arquitectura/01-design-system/`
|
||||
|
||||
### OpenSpec - Especificaciones del Proyecto
|
||||
| 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
|
||||
777
_openspec/specs/arquitectura-limpia.md
Normal file
777
_openspec/specs/arquitectura-limpia.md
Normal 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
|
||||
1235
_openspec/specs/estandares-codigo.md
Normal file
1235
_openspec/specs/estandares-codigo.md
Normal file
File diff suppressed because it is too large
Load Diff
687
_openspec/specs/nomenclatura.md
Normal file
687
_openspec/specs/nomenclatura.md
Normal 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
|
||||
@@ -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)
|
||||
199
_planificacion/01-design-system/02-FILOSOFIA-DE-DISENO.md
Normal file
199
_planificacion/01-design-system/02-FILOSOFIA-DE-DISENO.md
Normal 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)
|
||||
228
_planificacion/01-design-system/03-PALETA-DE-COLORES.md
Normal file
228
_planificacion/01-design-system/03-PALETA-DE-COLORES.md
Normal 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)
|
||||
336
_planificacion/01-design-system/04-TIPOGRAFIA.md
Normal file
336
_planificacion/01-design-system/04-TIPOGRAFIA.md
Normal 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)
|
||||
340
_planificacion/01-design-system/05-SISTEMA-GRID-ESPACIADO.md
Normal file
340
_planificacion/01-design-system/05-SISTEMA-GRID-ESPACIADO.md
Normal 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)
|
||||
366
_planificacion/01-design-system/06-ESTRUCTURA-LAYOUT.md
Normal file
366
_planificacion/01-design-system/06-ESTRUCTURA-LAYOUT.md
Normal 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)
|
||||
@@ -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)
|
||||
443
_planificacion/01-design-system/08-PATRONES-FORMULARIOS.md
Normal file
443
_planificacion/01-design-system/08-PATRONES-FORMULARIOS.md
Normal 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)
|
||||
360
_planificacion/01-design-system/09-VISTA-PREVIA-TIEMPO-REAL.md
Normal file
360
_planificacion/01-design-system/09-VISTA-PREVIA-TIEMPO-REAL.md
Normal 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)
|
||||
422
_planificacion/01-design-system/10-RESPONSIVE-DESIGN.md
Normal file
422
_planificacion/01-design-system/10-RESPONSIVE-DESIGN.md
Normal 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)
|
||||
548
_planificacion/01-design-system/11-JAVASCRIPT-PATTERNS.md
Normal file
548
_planificacion/01-design-system/11-JAVASCRIPT-PATTERNS.md
Normal 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)
|
||||
443
_planificacion/01-design-system/12-PERSISTENCIA-JSON.md
Normal file
443
_planificacion/01-design-system/12-PERSISTENCIA-JSON.md
Normal 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)
|
||||
473
_planificacion/01-design-system/13-PANEL-ADMINISTRACION.md
Normal file
473
_planificacion/01-design-system/13-PANEL-ADMINISTRACION.md
Normal 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)
|
||||
395
_planificacion/01-design-system/14-CONFLICTOS-WORDPRESS.md
Normal file
395
_planificacion/01-design-system/14-CONFLICTOS-WORDPRESS.md
Normal 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)
|
||||
420
_planificacion/01-design-system/15-EJEMPLOS-COMPLETOS.md
Normal file
420
_planificacion/01-design-system/15-EJEMPLOS-COMPLETOS.md
Normal 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)
|
||||
136
_planificacion/01-design-system/README.md
Normal file
136
_planificacion/01-design-system/README.md
Normal 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
|
||||
358
_planificacion/01-design-system/design-tokens-apus-admin.scss
Normal file
358
_planificacion/01-design-system/design-tokens-apus-admin.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
*/
|
||||
341
_planificacion/analisis-spam-formularios.md
Normal file
341
_planificacion/analisis-spam-formularios.md
Normal 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.
|
||||
|
||||
366
_planificacion/plan-mejora-especificaciones-openspec.md
Normal file
366
_planificacion/plan-mejora-especificaciones-openspec.md
Normal 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**
|
||||
1121
_planificacion/roi-theme-template/css/style.css
Normal file
1121
_planificacion/roi-theme-template/css/style.css
Normal file
File diff suppressed because it is too large
Load Diff
BIN
_planificacion/roi-theme-template/img/featured-image.png
Normal file
BIN
_planificacion/roi-theme-template/img/featured-image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 256 KiB |
1159
_planificacion/roi-theme-template/index.html
Normal file
1159
_planificacion/roi-theme-template/index.html
Normal file
File diff suppressed because it is too large
Load Diff
317
_planificacion/roi-theme-template/js/main.js
Normal file
317
_planificacion/roi-theme-template/js/main.js
Normal 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;');
|
||||
42
_planificacion/roi-theme-template/modal-contact.html
Normal file
42
_planificacion/roi-theme-template/modal-contact.html
Normal 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>
|
||||
335
_planificacion/validacion-specs-vs-codigo-actual.md
Normal file
335
_planificacion/validacion-specs-vs-codigo-actual.md
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 |
|
||||
Reference in New Issue
Block a user