refactor: reorganizar openspec y planificacion con spec recaptcha

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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