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:
@@ -0,0 +1,177 @@
|
||||
# ⚙️ ESPECIFICACIONES DEL SISTEMA
|
||||
|
||||
## Requerimientos Generales
|
||||
|
||||
- **Persistencia de Datos**: Archivos JSON (NO localStorage, NO base de datos)
|
||||
- **Vista Previa**: TODAS las pestañas DEBEN tener vista previa en tiempo real
|
||||
- **Panel Principal**: Existe un panel de administración que lista todas las pestañas disponibles
|
||||
- **Arquitectura**: Componentes independientes pero con diseño consistente
|
||||
- **Navegación**: Sistema de pestañas/tabs para alternar entre componentes
|
||||
|
||||
---
|
||||
|
||||
## Stack Tecnológico
|
||||
|
||||
### CDN y Librerías
|
||||
|
||||
```html
|
||||
<!-- Bootstrap 5.3.2 -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/Css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap Icons 1.11.3 -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/Font/bootstrap-icons.min.css">
|
||||
|
||||
<!-- Google Fonts: Poppins -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- CSS del proyecto -->
|
||||
<link rel="stylesheet" href="../../Css/style.css">
|
||||
```
|
||||
|
||||
### JavaScript
|
||||
|
||||
```html
|
||||
<!-- Bootstrap JS Bundle -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/Js/bootstrap.bundle.min.js"></script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Estructura de Archivos
|
||||
|
||||
**Arquitectura modular:** Cada componente tiene sus propios archivos separados (PHP, CSS, JS).
|
||||
|
||||
```
|
||||
wp-content/themes/apus-theme/
|
||||
└── admin-panel/
|
||||
└── Admin/
|
||||
├── Components/
|
||||
│ ├── component-[name].php
|
||||
│ ├── component-[name]-2.php
|
||||
│ └── component-[name]-3.php
|
||||
├── Assets/
|
||||
│ ├── Css/
|
||||
│ │ ├── component-[name].css
|
||||
│ │ ├── component-[name]-2.css
|
||||
│ │ └── component-[name]-3.css
|
||||
│ └── Js/
|
||||
│ ├── component-[name].js
|
||||
│ ├── component-[name]-2.js
|
||||
│ └── component-[name]-3.js
|
||||
└── Config/
|
||||
├── [name]-config.json
|
||||
├── [name]-2-config.json
|
||||
└── [name]-3-config.json
|
||||
```
|
||||
|
||||
**Ejemplo con un componente específico:**
|
||||
```
|
||||
admin-panel/Admin/
|
||||
├── Components/
|
||||
│ └── component-notification-bar.php
|
||||
├── Assets/
|
||||
│ ├── Css/
|
||||
│ │ └── component-notification-bar.css
|
||||
│ └── Js/
|
||||
│ └── component-notification-bar.js
|
||||
└── Config/
|
||||
└── notification-bar-config.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Convenciones de Nombrado
|
||||
|
||||
### Archivos PHP (Componentes)
|
||||
|
||||
- Patrón: `component-[nombre].php`
|
||||
- Ubicación: `admin-panel/Admin/Components/`
|
||||
- Ejemplo: `component-notification-bar.php`, `component-site-footer.php`
|
||||
|
||||
### Archivos CSS
|
||||
|
||||
- Patrón: `component-[nombre].css`
|
||||
- Ubicación: `admin-panel/Admin/Assets/Css/`
|
||||
- Mismo nombre base que el componente PHP correspondiente
|
||||
|
||||
### Archivos JavaScript
|
||||
|
||||
- Patrón: `component-[nombre].js`
|
||||
- Ubicación: `admin-panel/Admin/Assets/Js/`
|
||||
- Mismo nombre base que el componente PHP correspondiente
|
||||
|
||||
### Archivos de Configuración JSON
|
||||
|
||||
- Patrón: `[nombre]-config.json`
|
||||
- Ubicación: `admin-panel/Admin/Config/`
|
||||
- Ejemplo: `notification-bar-config.json`
|
||||
|
||||
---
|
||||
|
||||
## Dependencias del Proyecto
|
||||
|
||||
| Dependencia | Versión | Propósito |
|
||||
|-------------|---------|-----------|
|
||||
| Bootstrap | 5.3.2 | Framework CSS y componentes UI |
|
||||
| Bootstrap Icons | 1.11.3 | Sistema de iconos |
|
||||
| Poppins Font | Google Fonts | Tipografía principal |
|
||||
| style.css | Custom | Estilos específicos del proyecto |
|
||||
|
||||
---
|
||||
|
||||
## Requerimientos del Navegador
|
||||
|
||||
- **Navegadores modernos**: Chrome, Firefox, Safari, Edge (últimas 2 versiones)
|
||||
- **JavaScript**: Habilitado
|
||||
- **CSS Grid**: Soporte requerido
|
||||
- **Flexbox**: Soporte requerido
|
||||
- **CSS Custom Properties**: Soporte requerido
|
||||
|
||||
---
|
||||
|
||||
## Estructura de un Componente
|
||||
|
||||
Cada componente debe tener:
|
||||
|
||||
1. ✅ **Archivo PHP** (`component-[name].php`)
|
||||
- Formulario de configuración
|
||||
- Vista previa en tiempo real
|
||||
- Integración con WordPress
|
||||
|
||||
2. ✅ **Archivo CSS** (`component-[name].css`)
|
||||
- Estilos específicos del componente admin
|
||||
- Estilos para vista previa
|
||||
|
||||
3. ✅ **Archivo JavaScript** (`component-[name].js`)
|
||||
- Funcionalidad del componente
|
||||
- Event listeners
|
||||
- Funciones de actualización de preview
|
||||
- Persistencia de configuración
|
||||
|
||||
4. ✅ **Configuración JSON** (`[name]-config.json`)
|
||||
- Valores por defecto
|
||||
- Metadata del componente
|
||||
- Timestamp de última modificación
|
||||
|
||||
---
|
||||
|
||||
## Reglas de Integración
|
||||
|
||||
### ❌ NO Permitido
|
||||
|
||||
- localStorage para persistencia (usar config.json)
|
||||
- Base de datos directa
|
||||
- Inline styles que sobreescriban CSS del front-end
|
||||
- Modificar HTML del front-end desde admin
|
||||
|
||||
### ✅ Permitido
|
||||
|
||||
- Archivos JSON para configuración
|
||||
- Inline styles SOLO en el admin panel (si no afecta preview)
|
||||
- CSS con `!important` SOLO si es necesario para override de WordPress
|
||||
|
||||
---
|
||||
|
||||
## Volver al Índice
|
||||
|
||||
[← Volver al README](README.md)
|
||||
199
_planificacion/01-design-system/02-FILOSOFIA-DE-DISENO.md
Normal file
199
_planificacion/01-design-system/02-FILOSOFIA-DE-DISENO.md
Normal file
@@ -0,0 +1,199 @@
|
||||
# 🎯 FILOSOFÍA DE DISEÑO
|
||||
|
||||
## Principios Clave
|
||||
|
||||
### 1. Consistencia Visual
|
||||
Todos los componentes deben verse parte del mismo sistema.
|
||||
|
||||
**Implementación:**
|
||||
- Paleta de colores unificada (Navy + Orange)
|
||||
- Tipografía consistente (Poppins)
|
||||
- Iconos del mismo sistema (Bootstrap Icons)
|
||||
- Espaciado coherente entre componentes
|
||||
|
||||
### 2. Espaciado Compacto
|
||||
Diseño eficiente que maximiza el espacio sin sacrificar usabilidad.
|
||||
|
||||
**Implementación:**
|
||||
- Uso de `.form-control-sm` y `.btn-sm`
|
||||
- Padding ajustado en cards (`.p-3`)
|
||||
- Margins reducidos entre campos (`.mb-2`)
|
||||
- Grid compacto con `.g-3`
|
||||
|
||||
### 3. Jerarquía Clara
|
||||
Uso de colores, tamaños y pesos para guiar la atención.
|
||||
|
||||
**Jerarquía visual:**
|
||||
1. Header del Tab (Navy gradient + Orange icon)
|
||||
2. Títulos de Card (Navy + Bold)
|
||||
3. Labels de Campos (Neutral + Semibold + Orange icon)
|
||||
4. Texto de ayuda (Muted + Small)
|
||||
|
||||
### 4. Feedback Instantáneo
|
||||
Vista previa en tiempo real de todos los cambios.
|
||||
|
||||
**Implementación:**
|
||||
- Event listeners en todos los campos
|
||||
- Función `updatePreview()` conectada
|
||||
- Preview con HTML idéntico al front-end
|
||||
- Sin delay en la actualización
|
||||
|
||||
### 5. Mobile-First
|
||||
Diseño responsive que funciona en todos los dispositivos.
|
||||
|
||||
**Implementación:**
|
||||
- Grid con breakpoint `col-lg-6`
|
||||
- Stack vertical en mobile (<992px)
|
||||
- Headers responsive con `flex-wrap`
|
||||
- Botones full-width en mobile
|
||||
|
||||
---
|
||||
|
||||
## Características Comunes
|
||||
|
||||
Todos los componentes admin DEBEN incluir:
|
||||
|
||||
- ✅ **Vista previa en tiempo real**
|
||||
- Card con border-left orange
|
||||
- HTML idéntico al front-end
|
||||
- Botones Desktop/Mobile
|
||||
|
||||
- ✅ **Validación de formularios**
|
||||
- Campos requeridos marcados con `*`
|
||||
- Validación en JavaScript
|
||||
- Mensajes de error claros
|
||||
|
||||
- ✅ **Contador de caracteres en campos de texto**
|
||||
- Display: `<span id="fieldCount">0</span>/250`
|
||||
- Progress bar orange
|
||||
- Actualización en tiempo real
|
||||
|
||||
- ✅ **Botón de reseteo a valores por defecto**
|
||||
- En el header del tab
|
||||
- Confirmación con `confirm()`
|
||||
- Restaura todos los campos
|
||||
|
||||
- ✅ **Tooltips informativos**
|
||||
- Links a documentación externa
|
||||
- Hints con `<small class="text-muted">`
|
||||
- Badges para información adicional
|
||||
|
||||
- ✅ **Iconos descriptivos en cada campo**
|
||||
- Color orange (#FF8600)
|
||||
- Bootstrap Icons
|
||||
- Clase `.me-2` para espaciado
|
||||
|
||||
- ✅ **Feedback visual inmediato**
|
||||
- Cambios reflejados sin delay
|
||||
- Console.logs informativos
|
||||
- Notificaciones de guardado
|
||||
|
||||
---
|
||||
|
||||
## Flujo de Interacción
|
||||
|
||||
### 1. Carga Inicial
|
||||
```
|
||||
Usuario abre admin panel
|
||||
↓
|
||||
loadConfig() carga valores guardados
|
||||
↓
|
||||
updatePreview() renderiza preview
|
||||
↓
|
||||
Panel listo para edición
|
||||
```
|
||||
|
||||
### 2. Edición de Campo
|
||||
```
|
||||
Usuario modifica campo
|
||||
↓
|
||||
Event listener detecta cambio
|
||||
↓
|
||||
updatePreview() actualiza preview
|
||||
↓
|
||||
saveConfig() guarda en localStorage (opcional)
|
||||
```
|
||||
|
||||
### 3. Guardado Final
|
||||
```
|
||||
Usuario confirma cambios (si hay botón guardar)
|
||||
↓
|
||||
validateForm() verifica datos
|
||||
↓
|
||||
saveConfig() guarda en config.json
|
||||
↓
|
||||
showNotification() confirma guardado
|
||||
```
|
||||
|
||||
### 4. Reset
|
||||
```
|
||||
Usuario presiona "Restaurar valores por defecto"
|
||||
↓
|
||||
confirm() pide confirmación
|
||||
↓
|
||||
resetToDefaults() aplica valores default
|
||||
↓
|
||||
updatePreview() actualiza preview
|
||||
↓
|
||||
saveConfig() guarda cambios
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Principios de Accesibilidad
|
||||
|
||||
### Labels y Formularios
|
||||
- Todos los inputs tienen `<label for="id">`
|
||||
- Labels descriptivos y claros
|
||||
- Campos requeridos marcados visualmente
|
||||
|
||||
### Navegación por Teclado
|
||||
- Tab order lógico
|
||||
- Focus visible en elementos interactivos
|
||||
- Enter para submit (si aplica)
|
||||
|
||||
### Contraste de Color
|
||||
- Texto principal: #495057 sobre fondo blanco (AAA)
|
||||
- Texto en headers: Blanco sobre navy (#0E2337) (AAA)
|
||||
- Links: Orange (#FF8600) con hover (#FF6B35)
|
||||
|
||||
### ARIA Attributes
|
||||
- Progress bars con `role="progressbar"`, `aria-valuenow`, etc.
|
||||
- Button groups con `role="group"`
|
||||
- Inputs con `aria-label` o `<label>`
|
||||
|
||||
---
|
||||
|
||||
## Mensajes y Comunicación
|
||||
|
||||
### Console.logs
|
||||
```javascript
|
||||
// ✅ USAR para debugging
|
||||
console.log('✅ [ComponentName] Admin Panel cargado');
|
||||
console.log('💾 Configuración guardada:', config);
|
||||
console.log('📂 Configuración cargada:', config);
|
||||
console.log('🔄 Valores por defecto restaurados');
|
||||
|
||||
// ❌ NO USAR en producción excesivamente
|
||||
console.log('Campo actualizado'); // Demasiado verboso
|
||||
```
|
||||
|
||||
### Notificaciones al Usuario
|
||||
```javascript
|
||||
// Success
|
||||
showNotification('Cambios guardados', 'success');
|
||||
|
||||
// Error
|
||||
showNotification('Error al guardar cambios', 'error');
|
||||
|
||||
// Confirmaciones
|
||||
if (!confirm('¿Estás seguro de restaurar los valores por defecto?')) {
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Volver al Índice
|
||||
|
||||
[← Volver al README](README.md)
|
||||
228
_planificacion/01-design-system/03-PALETA-DE-COLORES.md
Normal file
228
_planificacion/01-design-system/03-PALETA-DE-COLORES.md
Normal file
@@ -0,0 +1,228 @@
|
||||
# 🎨 PALETA DE COLORES
|
||||
|
||||
## Colores Principales
|
||||
|
||||
### Navy Brand Colors
|
||||
|
||||
```css
|
||||
--color-navy-dark: #0E2337; /* Fondo principal, headers */
|
||||
--color-navy-primary: #1e3a5f; /* Títulos, bordes importantes */
|
||||
--color-navy-light: #2c5282; /* Variaciones secundarias */
|
||||
```
|
||||
|
||||
**Uso:**
|
||||
- **#0E2337**: Fondo de gradientes, headers principales
|
||||
- **#1e3a5f**: Títulos de cards, border-left de cards
|
||||
- **#2c5282**: Variaciones y estados hover (opcional)
|
||||
|
||||
### Orange Accent Colors
|
||||
|
||||
```css
|
||||
--color-orange-primary: #FF8600; /* Acción primaria, iconos destacados */
|
||||
--color-orange-hover: #FF6B35; /* Hover states */
|
||||
--color-orange-light: #FFB800; /* Badges, alerts suaves */
|
||||
```
|
||||
|
||||
**Uso:**
|
||||
- **#FF8600**: Iconos, border-left de preview, botones primarios
|
||||
- **#FF6B35**: Hover en botones y links
|
||||
- **#FFB800**: Badges informativos, highlights
|
||||
|
||||
### Neutral Colors
|
||||
|
||||
```css
|
||||
--color-neutral-50: #f8f9fa; /* Fondo general del body */
|
||||
--color-neutral-100: #e9ecef; /* Bordes, separadores */
|
||||
--color-neutral-600: #495057; /* Texto principal */
|
||||
--color-neutral-700: #6c757d; /* Texto secundario */
|
||||
```
|
||||
|
||||
**Uso:**
|
||||
- **#f8f9fa**: Background del body
|
||||
- **#e9ecef**: Bordes sutiles, separadores
|
||||
- **#495057**: Texto de labels y contenido principal
|
||||
- **#6c757d**: Texto secundario, hints
|
||||
|
||||
---
|
||||
|
||||
## Uso de Colores por Elemento
|
||||
|
||||
### Tabla de Referencia
|
||||
|
||||
| Elemento | Color | Uso |
|
||||
|----------|-------|-----|
|
||||
| **Header del Tab** | Gradiente `#0E2337` → `#1e3a5f` | Encabezado principal de cada pestaña |
|
||||
| **Títulos de Card** | `#1e3a5f` | Títulos de secciones dentro de cards |
|
||||
| **Iconos Principales** | `#FF8600` | Todos los iconos destacados |
|
||||
| **Bordes Importantes** | `#1e3a5f` (izquierda 4px) | Border-left de cards |
|
||||
| **Bordes Especiales** | `#FF8600` (izquierda 4px) | Cards de vista previa |
|
||||
| **Texto Labels** | `#495057` | Labels de formularios |
|
||||
| **Badges** | `#FFB800` (fondo) + `#000` (texto) | Badges informativos |
|
||||
| **Links** | `#FF8600` | Enlaces de ayuda |
|
||||
| **Links Hover** | `#FF6B35` | Estado hover de enlaces |
|
||||
| **Botón Primario** | `#FF8600` | Acciones principales |
|
||||
| **Botón Primario Hover** | `#FF6B35` | Hover en botón primario |
|
||||
| **Fondo Body** | `#f8f9fa` | Fondo general de la página |
|
||||
|
||||
---
|
||||
|
||||
## Ejemplos de Implementación
|
||||
|
||||
### Header del Tab
|
||||
|
||||
```html
|
||||
<div class="rounded p-4 mb-4 shadow text-white"
|
||||
style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%);
|
||||
border-left: 4px solid #FF8600;">
|
||||
<h3 class="h4 mb-1 fw-bold">
|
||||
<i class="bi bi-megaphone-fill me-2" style="color: #FF8600;"></i>
|
||||
Título del Componente
|
||||
</h3>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Card Estándar
|
||||
|
||||
```html
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-palette me-2" style="color: #FF8600;"></i>
|
||||
Título de Sección
|
||||
</h5>
|
||||
<!-- Contenido -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Card de Vista Previa
|
||||
|
||||
```html
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #FF8600;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-eye me-2" style="color: #FF8600;"></i>
|
||||
Vista Previa en Tiempo Real
|
||||
</h5>
|
||||
<!-- Preview -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Label con Icono
|
||||
|
||||
```html
|
||||
<label for="fieldId" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-paint-bucket me-1" style="color: #FF8600;"></i>
|
||||
Nombre del Campo
|
||||
</label>
|
||||
```
|
||||
|
||||
### Badge Informativo
|
||||
|
||||
```html
|
||||
<span class="badge text-dark" style="background-color: #FFB800; font-size: 0.65rem;">
|
||||
Opcional
|
||||
</span>
|
||||
```
|
||||
|
||||
### Link de Ayuda
|
||||
|
||||
```html
|
||||
<small class="text-muted d-block mt-1">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Ver: <a href="https://ejemplo.com" target="_blank"
|
||||
class="text-decoration-none" style="color: #FF8600;">
|
||||
Documentación <i class="bi bi-box-arrow-up-right"></i>
|
||||
</a>
|
||||
</small>
|
||||
```
|
||||
|
||||
### Botón Primario
|
||||
|
||||
```css
|
||||
.btn-primary {
|
||||
background-color: #FF8600;
|
||||
border-color: #FF8600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #FF6B35;
|
||||
border-color: #FF6B35;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CSS con Variables
|
||||
|
||||
### Definición de Variables
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Navy Colors */
|
||||
--navy-dark: #0E2337;
|
||||
--navy-primary: #1e3a5f;
|
||||
--navy-light: #2c5282;
|
||||
|
||||
/* Orange Colors */
|
||||
--orange-primary: #FF8600;
|
||||
--orange-hover: #FF6B35;
|
||||
--orange-light: #FFB800;
|
||||
|
||||
/* Neutral Colors */
|
||||
--neutral-50: #f8f9fa;
|
||||
--neutral-100: #e9ecef;
|
||||
--neutral-600: #495057;
|
||||
--neutral-700: #6c757d;
|
||||
}
|
||||
```
|
||||
|
||||
### Uso de Variables
|
||||
|
||||
```css
|
||||
/* Header del Tab */
|
||||
.tab-header {
|
||||
background: linear-gradient(135deg, var(--navy-dark) 0%, var(--navy-primary) 100%);
|
||||
border-left: 4px solid var(--orange-primary);
|
||||
}
|
||||
|
||||
/* Iconos */
|
||||
.tab-header i,
|
||||
.card-title i,
|
||||
label i {
|
||||
color: var(--orange-primary);
|
||||
}
|
||||
|
||||
/* Títulos de Card */
|
||||
.card-title {
|
||||
color: var(--navy-primary);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Contraste y Accesibilidad
|
||||
|
||||
### Ratios de Contraste (WCAG)
|
||||
|
||||
| Combinación | Ratio | Nivel WCAG |
|
||||
|-------------|-------|------------|
|
||||
| `#0E2337` sobre blanco | 12.6:1 | AAA |
|
||||
| `#1e3a5f` sobre blanco | 8.2:1 | AAA |
|
||||
| `#FF8600` sobre blanco | 3.4:1 | AA (large text) |
|
||||
| Blanco sobre `#0E2337` | 12.6:1 | AAA |
|
||||
| `#495057` sobre blanco | 7.8:1 | AAA |
|
||||
|
||||
**Recomendaciones:**
|
||||
- ✅ Texto principal: `#495057` sobre blanco
|
||||
- ✅ Texto en headers: Blanco sobre `#0E2337`
|
||||
- ✅ Links: `#FF8600` (usar bold o underline para mejor accesibilidad)
|
||||
- ✅ Títulos: `#1e3a5f` sobre blanco
|
||||
|
||||
---
|
||||
|
||||
## Volver al Índice
|
||||
|
||||
[← Volver al README](README.md)
|
||||
336
_planificacion/01-design-system/04-TIPOGRAFIA.md
Normal file
336
_planificacion/01-design-system/04-TIPOGRAFIA.md
Normal file
@@ -0,0 +1,336 @@
|
||||
# 📝 TIPOGRAFÍA
|
||||
|
||||
## Font Stack
|
||||
|
||||
```css
|
||||
font-family: 'Poppins', sans-serif;
|
||||
```
|
||||
|
||||
**Carga desde Google Fonts:**
|
||||
```html
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pesos y Tamaños
|
||||
|
||||
### Headers
|
||||
|
||||
```css
|
||||
/* Título principal del tab */
|
||||
.h3, h3 {
|
||||
font-size: 1.5rem; /* 24px */
|
||||
font-weight: 700; /* Bold */
|
||||
}
|
||||
|
||||
/* Subtítulos grandes */
|
||||
.h4, h4 {
|
||||
font-size: 1.25rem; /* 20px */
|
||||
font-weight: 700; /* Bold */
|
||||
}
|
||||
|
||||
/* Títulos de cards */
|
||||
.h5, h5 {
|
||||
font-size: 1rem; /* 16px */
|
||||
font-weight: 700; /* Bold */
|
||||
}
|
||||
```
|
||||
|
||||
### Body Text
|
||||
|
||||
```css
|
||||
/* Labels, descripciones */
|
||||
.small {
|
||||
font-size: 0.875rem; /* 14px */
|
||||
}
|
||||
|
||||
/* Hints, contadores */
|
||||
.text-muted {
|
||||
font-size: 0.75rem; /* 12px */
|
||||
}
|
||||
```
|
||||
|
||||
### Font Weights
|
||||
|
||||
```css
|
||||
.fw-bold {
|
||||
font-weight: 700; /* Títulos principales */
|
||||
}
|
||||
|
||||
.fw-semibold {
|
||||
font-weight: 600; /* Labels importantes */
|
||||
}
|
||||
|
||||
.fw-normal {
|
||||
font-weight: 400; /* Texto normal */
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Jerarquía Visual
|
||||
|
||||
### Nivel 1: Título del Tab
|
||||
|
||||
```html
|
||||
<h3 class="h4 mb-1 fw-bold" style="color: #0E2337;">
|
||||
<i class="bi bi-megaphone-fill me-2" style="color: #FF8600;"></i>
|
||||
Configuración del Top Bar
|
||||
</h3>
|
||||
```
|
||||
|
||||
**Características:**
|
||||
- Color: Navy Dark (#0E2337) o Blanco (en gradiente)
|
||||
- Peso: Bold (700)
|
||||
- Tamaño: 1.25rem (h4 en h3)
|
||||
- Icono: Orange (#FF8600)
|
||||
|
||||
### Nivel 2: Título de Card
|
||||
|
||||
```html
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-palette me-2" style="color: #FF8600;"></i>
|
||||
Colores y Estilos
|
||||
</h5>
|
||||
```
|
||||
|
||||
**Características:**
|
||||
- Color: Navy Primary (#1e3a5f)
|
||||
- Peso: Bold (700)
|
||||
- Tamaño: 1rem
|
||||
- Icono: Orange (#FF8600)
|
||||
|
||||
### Nivel 3: Label de Campo
|
||||
|
||||
```html
|
||||
<label for="fieldId" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-paint-bucket me-1" style="color: #FF8600;"></i>
|
||||
Color de Fondo
|
||||
</label>
|
||||
```
|
||||
|
||||
**Características:**
|
||||
- Color: Neutral 600 (#495057)
|
||||
- Peso: Semibold (600)
|
||||
- Tamaño: 0.875rem (small)
|
||||
- Icono: Orange (#FF8600)
|
||||
|
||||
### Nivel 4: Texto de Ayuda
|
||||
|
||||
```html
|
||||
<small class="text-muted d-block mt-1">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Este campo es opcional
|
||||
</small>
|
||||
```
|
||||
|
||||
**Características:**
|
||||
- Color: Muted (Bootstrap default)
|
||||
- Peso: Normal (400)
|
||||
- Tamaño: 0.75rem
|
||||
- Display: Block
|
||||
|
||||
---
|
||||
|
||||
## Ejemplos de Uso
|
||||
|
||||
### Header del Tab Completo
|
||||
|
||||
```html
|
||||
<div class="rounded p-4 mb-4 shadow text-white"
|
||||
style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%);">
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
|
||||
<div>
|
||||
<!-- Título principal -->
|
||||
<h3 class="h4 mb-1 fw-bold">
|
||||
<i class="bi bi-megaphone-fill me-2" style="color: #FF8600;"></i>
|
||||
Configuración del Top Bar
|
||||
</h3>
|
||||
<!-- Descripción -->
|
||||
<p class="mb-0 small" style="opacity: 0.85;">
|
||||
Personaliza la barra de anuncios superior del sitio
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-light">
|
||||
<i class="bi bi-arrow-counterclockwise me-1"></i>
|
||||
Restaurar valores por defecto
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Card con Título
|
||||
|
||||
```html
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<!-- Título de sección -->
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-palette me-2" style="color: #FF8600;"></i>
|
||||
Colores y Estilos
|
||||
</h5>
|
||||
|
||||
<!-- Contenido del card -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Campo de Formulario Completo
|
||||
|
||||
```html
|
||||
<div class="mb-2">
|
||||
<!-- Label con icono -->
|
||||
<label for="messageText" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-chat-text me-1" style="color: #FF8600;"></i>
|
||||
Mensaje Principal <span class="text-danger">*</span>
|
||||
<span class="float-end text-muted">
|
||||
<span id="messageTextCount" class="fw-bold">0</span>/250
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<!-- Input -->
|
||||
<textarea id="messageText"
|
||||
class="form-control form-control-sm"
|
||||
rows="2"
|
||||
maxlength="250"
|
||||
required></textarea>
|
||||
|
||||
<!-- Hint -->
|
||||
<small class="text-muted d-block mt-1">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Este mensaje se mostrará en la barra superior del sitio
|
||||
</small>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Line Height y Espaciado
|
||||
|
||||
### Line Heights Recomendados
|
||||
|
||||
```css
|
||||
/* Títulos */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* Body text */
|
||||
p, .form-label, .small {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Text muted (hints) */
|
||||
.text-muted {
|
||||
line-height: 1.4;
|
||||
}
|
||||
```
|
||||
|
||||
### Letter Spacing
|
||||
|
||||
```css
|
||||
/* Títulos principales */
|
||||
h3, .h3 {
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
/* Labels */
|
||||
.form-label {
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
/* Texto normal */
|
||||
body {
|
||||
letter-spacing: 0;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Responsive Typography
|
||||
|
||||
### Desktop (≥992px)
|
||||
|
||||
```css
|
||||
@media (min-width: 992px) {
|
||||
.h3, h3 {
|
||||
font-size: 1.5rem; /* 24px */
|
||||
}
|
||||
|
||||
.h4, h4 {
|
||||
font-size: 1.25rem; /* 20px */
|
||||
}
|
||||
|
||||
.h5, h5 {
|
||||
font-size: 1rem; /* 16px */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tablet (576px - 991px)
|
||||
|
||||
```css
|
||||
@media (max-width: 991px) {
|
||||
.tab-header h3 {
|
||||
font-size: 1.1rem; /* 17.6px */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Mobile (<576px)
|
||||
|
||||
```css
|
||||
@media (max-width: 575px) {
|
||||
.tab-header h3 {
|
||||
font-size: 1rem; /* 16px */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Clases Utility de Tipografía
|
||||
|
||||
### Tamaños
|
||||
|
||||
```html
|
||||
<p class="fs-1">Font size 1 (largest)</p>
|
||||
<p class="fs-2">Font size 2</p>
|
||||
<p class="fs-3">Font size 3</p>
|
||||
<p class="fs-4">Font size 4</p>
|
||||
<p class="fs-5">Font size 5</p>
|
||||
<p class="fs-6">Font size 6 (smallest)</p>
|
||||
```
|
||||
|
||||
### Pesos
|
||||
|
||||
```html
|
||||
<p class="fw-bold">Bold text (700)</p>
|
||||
<p class="fw-semibold">Semibold text (600)</p>
|
||||
<p class="fw-normal">Normal weight (400)</p>
|
||||
<p class="fw-light">Light weight (300)</p>
|
||||
```
|
||||
|
||||
### Estilos
|
||||
|
||||
```html
|
||||
<p class="fst-italic">Italic text</p>
|
||||
<p class="fst-normal">Normal style</p>
|
||||
<p class="text-decoration-underline">Underlined</p>
|
||||
<p class="text-decoration-line-through">Strikethrough</p>
|
||||
```
|
||||
|
||||
### Transformaciones
|
||||
|
||||
```html
|
||||
<p class="text-lowercase">LOWERCASED TEXT</p>
|
||||
<p class="text-uppercase">uppercased text</p>
|
||||
<p class="text-capitalize">capitalized text</p>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Volver al Índice
|
||||
|
||||
[← Volver al README](README.md)
|
||||
340
_planificacion/01-design-system/05-SISTEMA-GRID-ESPACIADO.md
Normal file
340
_planificacion/01-design-system/05-SISTEMA-GRID-ESPACIADO.md
Normal file
@@ -0,0 +1,340 @@
|
||||
# 📏 SISTEMA DE GRID Y ESPACIADO
|
||||
|
||||
## Grid de Bootstrap
|
||||
|
||||
### Sistema de 12 Columnas
|
||||
|
||||
```css
|
||||
.container-fluid {
|
||||
max-width: 1400px; /* Máximo ancho del admin panel */
|
||||
padding-right: var(--bs-gutter-x, 0.75rem);
|
||||
padding-left: var(--bs-gutter-x, 0.75rem);
|
||||
}
|
||||
|
||||
.row {
|
||||
--bs-gutter-x: 1.5rem; /* Espacio horizontal entre columnas */
|
||||
--bs-gutter-y: 0; /* Espacio vertical */
|
||||
}
|
||||
```
|
||||
|
||||
### Uso en Admin Panels
|
||||
|
||||
```css
|
||||
.row.g-3 { /* Gap de 1rem (16px) */
|
||||
--bs-gutter-x: 1rem;
|
||||
--bs-gutter-y: 1rem;
|
||||
}
|
||||
|
||||
.row.g-2 { /* Gap de 0.5rem (8px) */
|
||||
--bs-gutter-x: 0.5rem;
|
||||
--bs-gutter-y: 0.5rem;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Breakpoints
|
||||
|
||||
| Breakpoint | Min Width | Max Width | Dispositivo | Comportamiento |
|
||||
|------------|-----------|-----------|-------------|----------------|
|
||||
| **xs** | 0px | 575px | Móvil | Stack vertical |
|
||||
| **sm** | 576px | 767px | Tablet pequeña | Stack vertical |
|
||||
| **md** | 768px | 991px | Tablet | Stack vertical |
|
||||
| **lg** | 992px | 1199px | Desktop | 2 columnas |
|
||||
| **xl** | 1200px | 1399px | Desktop grande | 2 columnas |
|
||||
| **xxl** | 1400px | ∞ | Desktop XL | 2 columnas |
|
||||
|
||||
### Punto de Quiebre Principal: lg (992px)
|
||||
|
||||
```html
|
||||
<!-- 2 columnas en desktop (≥992px), stack en mobile/tablet (<992px) -->
|
||||
<div class="col-lg-6">Columna 1</div>
|
||||
<div class="col-lg-6">Columna 2</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Clases de Grid Comunes
|
||||
|
||||
### Layout de 2 Columnas (Admin Panel Estándar)
|
||||
|
||||
```html
|
||||
<div class="row g-3">
|
||||
<!-- Columna izquierda -->
|
||||
<div class="col-lg-6">
|
||||
<!-- Cards de configuración -->
|
||||
</div>
|
||||
|
||||
<!-- Columna derecha -->
|
||||
<div class="col-lg-6">
|
||||
<!-- Cards de configuración -->
|
||||
</div>
|
||||
|
||||
<!-- Fila completa (opcional) -->
|
||||
<div class="col-12">
|
||||
<!-- Vista previa o cards full-width -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Layout de 3 Columnas Desiguales
|
||||
|
||||
```html
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-5">Campo 1</div>
|
||||
<div class="col-5">Campo 2</div>
|
||||
<div class="col-2">Campo 3</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Layout de 2 Columnas Iguales (Campos de Color)
|
||||
|
||||
```html
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label>Color Fondo</label>
|
||||
<input type="color" class="form-control form-control-color w-100">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label>Color Texto</label>
|
||||
<input type="color" class="form-control form-control-color w-100">
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Full Width
|
||||
|
||||
```html
|
||||
<div class="col-12">
|
||||
<!-- Contenido full-width -->
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sistema de Espaciado
|
||||
|
||||
### Escala de Espaciado (rem)
|
||||
|
||||
Bootstrap utiliza una escala basada en `rem`:
|
||||
|
||||
```css
|
||||
/* Bootstrap Spacing Scale */
|
||||
0 = 0
|
||||
1 = 0.25rem (4px)
|
||||
2 = 0.5rem (8px)
|
||||
3 = 1rem (16px)
|
||||
4 = 1.5rem (24px)
|
||||
5 = 3rem (48px)
|
||||
```
|
||||
|
||||
### Margin (m) y Padding (p)
|
||||
|
||||
#### Margin
|
||||
|
||||
```html
|
||||
<!-- Margin Bottom -->
|
||||
.mb-2 /* margin-bottom: 0.5rem (campos de formulario) */
|
||||
.mb-3 /* margin-bottom: 1rem (cards, secciones) */
|
||||
.mb-4 /* margin-bottom: 1.5rem (headers) */
|
||||
|
||||
<!-- Margin Right -->
|
||||
.me-2 /* margin-right: 0.5rem (iconos) */
|
||||
.me-1 /* margin-right: 0.25rem (iconos pequeños) */
|
||||
|
||||
<!-- Margin Left -->
|
||||
.ms-2 /* margin-left: 0.5rem */
|
||||
|
||||
<!-- Margin Top -->
|
||||
.mt-1 /* margin-top: 0.25rem (hints) */
|
||||
.mt-3 /* margin-top: 1rem (separadores) */
|
||||
|
||||
<!-- Margin 0 (reset) -->
|
||||
.m-0 /* margin: 0 */
|
||||
.mb-0 /* margin-bottom: 0 */
|
||||
```
|
||||
|
||||
#### Padding
|
||||
|
||||
```html
|
||||
<!-- Padding uniforme -->
|
||||
.p-4 /* padding: 1.5rem (header del tab) */
|
||||
.p-3 /* padding: 1rem (card-body estándar) */
|
||||
|
||||
<!-- Padding vertical -->
|
||||
.py-4 /* padding-top y bottom: 1.5rem (container principal) */
|
||||
.py-3 /* padding-top y bottom: 1rem */
|
||||
|
||||
<!-- Padding horizontal -->
|
||||
.px-3 /* padding-left y right: 1rem */
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Uso Estándar en Admin Components
|
||||
|
||||
### Tabla de Referencia
|
||||
|
||||
| Elemento | Clase | Valor Real | Uso |
|
||||
|----------|-------|------------|-----|
|
||||
| **Container Principal** | `.py-4` | 1.5rem (24px) | Padding vertical del contenedor |
|
||||
| **Header del Tab** | `.mb-4` | 1.5rem (24px) | Separación después del header |
|
||||
| **Cards** | `.mb-3` | 1rem (16px) | Separación entre cards |
|
||||
| **Campos de Formulario** | `.mb-2` | 0.5rem (8px) | Separación entre campos |
|
||||
| **Card Body** | `.p-3` | 1rem (16px) | Padding interno de cards |
|
||||
| **Iconos en Título** | `.me-2` | 0.5rem (8px) | Espacio después de iconos grandes |
|
||||
| **Iconos en Label** | `.me-1` | 0.25rem (4px) | Espacio después de iconos pequeños |
|
||||
| **Títulos de Card** | `.mb-3` | 1rem (16px) | Separación después del título |
|
||||
| **Hints/Small Text** | `.mt-1` | 0.25rem (4px) | Espacio antes de hints |
|
||||
|
||||
---
|
||||
|
||||
## Gap Utilities
|
||||
|
||||
### Flexbox/Grid Gap
|
||||
|
||||
```html
|
||||
<!-- Gap en flex containers -->
|
||||
<div class="d-flex gap-2"> /* 0.5rem (8px) entre elementos */
|
||||
<div class="d-flex gap-3"> /* 1rem (16px) entre elementos */
|
||||
|
||||
<!-- Gap en grid (row) -->
|
||||
<div class="row g-2"> /* 0.5rem (8px) entre columnas */
|
||||
<div class="row g-3"> /* 1rem (16px) entre columnas */
|
||||
```
|
||||
|
||||
### Ejemplo: Header Responsive con Gap
|
||||
|
||||
```html
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
|
||||
<div>
|
||||
<h3 class="h4 mb-1 fw-bold">Título</h3>
|
||||
<p class="mb-0 small">Descripción</p>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-light">Botón</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Qué hace:**
|
||||
- `gap-3`: 1rem de espacio entre elementos hijos
|
||||
- `flex-wrap`: Permite que el botón baje a la siguiente línea en mobile
|
||||
- Mantiene espaciado consistente sin margins complejos
|
||||
|
||||
---
|
||||
|
||||
## Auto Layout
|
||||
|
||||
### Margin Auto
|
||||
|
||||
```html
|
||||
<!-- Centrar horizontalmente -->
|
||||
.mx-auto /* margin-left: auto; margin-right: auto; */
|
||||
|
||||
<!-- Empujar a la derecha -->
|
||||
.ms-auto /* margin-left: auto; */
|
||||
```
|
||||
|
||||
### Padding Auto (No existe en Bootstrap)
|
||||
|
||||
Bootstrap NO tiene padding auto. Usar flexbox para alineación:
|
||||
|
||||
```html
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>Izquierda</div>
|
||||
<div>Derecha</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ejemplos Completos
|
||||
|
||||
### Container Principal del Admin
|
||||
|
||||
```html
|
||||
<div class="container-fluid py-4" style="max-width: 1400px;">
|
||||
<!-- py-4: 1.5rem (24px) padding vertical -->
|
||||
<!-- max-width: 1400px para no ser demasiado ancho -->
|
||||
</div>
|
||||
```
|
||||
|
||||
### Header del Tab
|
||||
|
||||
```html
|
||||
<div class="rounded p-4 mb-4 shadow text-white"
|
||||
style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%);">
|
||||
<!-- p-4: 1.5rem (24px) padding uniforme -->
|
||||
<!-- mb-4: 1.5rem (24px) margin-bottom -->
|
||||
</div>
|
||||
```
|
||||
|
||||
### Card Estándar
|
||||
|
||||
```html
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body p-3">
|
||||
<!-- mb-3: 1rem (16px) margin-bottom del card -->
|
||||
<!-- p-3: 1rem (16px) padding del body -->
|
||||
|
||||
<h5 class="fw-bold mb-3">Título</h5>
|
||||
<!-- mb-3: 1rem (16px) separación después del título -->
|
||||
|
||||
<div class="mb-2">
|
||||
<!-- mb-2: 0.5rem (8px) separación entre campos -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Campo de Formulario con Label e Icono
|
||||
|
||||
```html
|
||||
<div class="mb-2">
|
||||
<label for="field" class="form-label small mb-1 fw-semibold">
|
||||
<i class="bi bi-palette me-1" style="color: #FF8600;"></i>
|
||||
<!-- me-1: 0.25rem (4px) después del icono -->
|
||||
Nombre del Campo
|
||||
</label>
|
||||
<input type="text" id="field" class="form-control form-control-sm">
|
||||
<small class="text-muted d-block mt-1">
|
||||
<!-- mt-1: 0.25rem (4px) antes del hint -->
|
||||
Texto de ayuda
|
||||
</small>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Responsive Spacing
|
||||
|
||||
### Desktop (≥992px)
|
||||
|
||||
Usar espaciado estándar sin modificaciones.
|
||||
|
||||
### Mobile (<576px)
|
||||
|
||||
```css
|
||||
@media (max-width: 575px) {
|
||||
/* Reducir padding en containers */
|
||||
.container-fluid {
|
||||
padding-right: 0.5rem;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
/* Reducir padding en headers */
|
||||
.tab-header {
|
||||
padding: 0.75rem !important;
|
||||
}
|
||||
|
||||
/* Reducir padding en cards */
|
||||
.card-body {
|
||||
padding: 0.75rem !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Volver al Índice
|
||||
|
||||
[← Volver al README](README.md)
|
||||
366
_planificacion/01-design-system/06-ESTRUCTURA-LAYOUT.md
Normal file
366
_planificacion/01-design-system/06-ESTRUCTURA-LAYOUT.md
Normal file
@@ -0,0 +1,366 @@
|
||||
# 📐 ESTRUCTURA DE LAYOUT
|
||||
|
||||
## Container Principal
|
||||
|
||||
```html
|
||||
<div class="container-fluid py-4" style="max-width: 1400px;">
|
||||
<!-- Todo el contenido del admin panel va aquí -->
|
||||
</div>
|
||||
```
|
||||
|
||||
**Características:**
|
||||
- `container-fluid`: Ancho flexible con padding
|
||||
- `py-4`: 1.5rem (24px) padding vertical
|
||||
- `max-width: 1400px`: Limita el ancho máximo para legibilidad
|
||||
|
||||
---
|
||||
|
||||
## Header del Tab (OBLIGATORIO EN TODOS LOS COMPONENTES)
|
||||
|
||||
### Estructura Completa
|
||||
|
||||
```html
|
||||
<div class="rounded p-4 mb-4 shadow text-white"
|
||||
style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%);
|
||||
border-left: 4px solid #FF8600;">
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
|
||||
<div>
|
||||
<h3 class="h4 mb-1 fw-bold">
|
||||
<i class="bi bi-[ICON] me-2" style="color: #FF8600;"></i>
|
||||
[TÍTULO DEL COMPONENTE]
|
||||
</h3>
|
||||
<p class="mb-0 small" style="opacity: 0.85;">
|
||||
[Descripción breve del componente]
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-light" id="reset[Component]Defaults">
|
||||
<i class="bi bi-arrow-counterclockwise me-1"></i>
|
||||
Restaurar valores por defecto
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Elementos Clave
|
||||
|
||||
- ✅ **Gradiente navy como fondo**: `#0E2337` → `#1e3a5f`
|
||||
- ✅ **Border-left orange de 4px**: `#FF8600`
|
||||
- ✅ **Icono orange en el título**: `color: #FF8600`
|
||||
- ✅ **Botón de reset alineado a la derecha**
|
||||
- ✅ **Responsive**: `flex-wrap` con `gap-3` para mobile
|
||||
|
||||
### Iconos Comunes por Tipo de Componente
|
||||
|
||||
| Tipo de Componente | Icono Sugerido | Clase Bootstrap Icons |
|
||||
|--------------------|----------------|----------------------|
|
||||
| Notificaciones/Anuncios | Megáfono | `bi-megaphone-fill` |
|
||||
| Navegación/Menú | Barras | `bi-layout-text-window` |
|
||||
| Colores/Estilos | Paleta | `bi-palette` |
|
||||
| Textos/Contenido | Texto | `bi-chat-text` |
|
||||
| Configuración General | Engranaje | `bi-gear-fill` |
|
||||
| Imágenes/Media | Imagen | `bi-image` |
|
||||
| Formularios | Portapapeles | `bi-clipboard-check` |
|
||||
|
||||
---
|
||||
|
||||
## Sistema de Grid
|
||||
|
||||
### Layout de 2 Columnas (Estándar)
|
||||
|
||||
```html
|
||||
<div class="row g-3">
|
||||
<!-- COLUMNA IZQUIERDA -->
|
||||
<div class="col-lg-6">
|
||||
<!-- Card 1 -->
|
||||
<div class="card shadow-sm mb-3">...</div>
|
||||
|
||||
<!-- Card 2 -->
|
||||
<div class="card shadow-sm mb-3">...</div>
|
||||
</div>
|
||||
|
||||
<!-- COLUMNA DERECHA -->
|
||||
<div class="col-lg-6">
|
||||
<!-- Card 3 -->
|
||||
<div class="card shadow-sm mb-3">...</div>
|
||||
|
||||
<!-- Card 4 -->
|
||||
<div class="card shadow-sm mb-3">...</div>
|
||||
</div>
|
||||
|
||||
<!-- FILA COMPLETA (opcional) -->
|
||||
<div class="col-12">
|
||||
<!-- Card de Vista Previa -->
|
||||
<div class="card shadow-sm mb-3">...</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Breakpoints:**
|
||||
- `col-lg-6`: 2 columnas en pantallas ≥992px
|
||||
- Stack vertical automático en pantallas <992px
|
||||
- `g-3`: Gap de 1rem (16px) entre columnas
|
||||
|
||||
### Layout de 3 Columnas (Menos Común)
|
||||
|
||||
```html
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-4">Card 1</div>
|
||||
<div class="col-lg-4">Card 2</div>
|
||||
<div class="col-lg-4">Card 3</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Layout Full Width
|
||||
|
||||
```html
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<!-- Card de Vista Previa, Sección Especial -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Estructura Completa de un Admin Panel
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin: [Component Name]</title>
|
||||
|
||||
<!-- CDN Links -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/Css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/Font/bootstrap-icons.min.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="../../Css/style.css">
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Sobreescribir max-width de WordPress */
|
||||
body .card {
|
||||
max-width: none !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- CONTAINER PRINCIPAL -->
|
||||
<div class="container-fluid py-4" style="max-width: 1400px;">
|
||||
|
||||
<!-- HEADER DEL TAB -->
|
||||
<div class="rounded p-4 mb-4 shadow text-white"
|
||||
style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%);
|
||||
border-left: 4px solid #FF8600;">
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
|
||||
<div>
|
||||
<h3 class="h4 mb-1 fw-bold">
|
||||
<i class="bi bi-megaphone-fill me-2" style="color: #FF8600;"></i>
|
||||
[TÍTULO DEL COMPONENTE]
|
||||
</h3>
|
||||
<p class="mb-0 small" style="opacity: 0.85;">
|
||||
[Descripción breve]
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-light" id="resetDefaults">
|
||||
<i class="bi bi-arrow-counterclockwise me-1"></i>
|
||||
Restaurar valores por defecto
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- GRID DE CONTENIDO -->
|
||||
<div class="row g-3">
|
||||
|
||||
<!-- COLUMNA IZQUIERDA -->
|
||||
<div class="col-lg-6">
|
||||
<!-- Card de configuración 1 -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-palette me-2" style="color: #FF8600;"></i>
|
||||
Sección 1
|
||||
</h5>
|
||||
<!-- Campos -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- COLUMNA DERECHA -->
|
||||
<div class="col-lg-6">
|
||||
<!-- Card de configuración 2 -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-gear me-2" style="color: #FF8600;"></i>
|
||||
Sección 2
|
||||
</h5>
|
||||
<!-- Campos -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- VISTA PREVIA (Full Width) -->
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #FF8600;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-eye me-2" style="color: #FF8600;"></i>
|
||||
Vista Previa en Tiempo Real
|
||||
</h5>
|
||||
|
||||
<!-- Preview del componente -->
|
||||
<div id="componentPreview" class="[component-class]">
|
||||
<!-- HTML idéntico al front-end -->
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||
<small class="text-muted">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Los cambios se reflejan en tiempo real
|
||||
</small>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button type="button" class="btn btn-outline-secondary active" id="previewDesktop">
|
||||
<i class="bi bi-display"></i> Desktop
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" id="previewMobile">
|
||||
<i class="bi bi-phone"></i> Mobile
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/Js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<script>
|
||||
// JavaScript del componente
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('✅ Admin Panel cargado');
|
||||
loadConfig();
|
||||
updatePreview();
|
||||
initializeEventListeners();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cards de Configuración
|
||||
|
||||
### Card Estándar
|
||||
|
||||
```html
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-palette me-2" style="color: #FF8600;"></i>
|
||||
[TÍTULO DE LA SECCIÓN]
|
||||
</h5>
|
||||
|
||||
<!-- Campos de formulario -->
|
||||
<div class="mb-2">
|
||||
<!-- Campo 1 -->
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<!-- Campo 2 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Card de Vista Previa
|
||||
|
||||
```html
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #FF8600;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-eye me-2" style="color: #FF8600;"></i>
|
||||
Vista Previa en Tiempo Real
|
||||
</h5>
|
||||
|
||||
<!-- Preview -->
|
||||
<div id="componentPreview" class="[component-class]">
|
||||
<!-- HTML idéntico al front-end -->
|
||||
</div>
|
||||
|
||||
<!-- Controles -->
|
||||
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||
<small class="text-muted">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Los cambios se reflejan en tiempo real
|
||||
</small>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button type="button" class="btn btn-outline-secondary active" id="previewDesktop">
|
||||
<i class="bi bi-display"></i> Desktop
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" id="previewMobile">
|
||||
<i class="bi bi-phone"></i> Mobile
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Diferencia clave:** Border-left ORANGE (#FF8600) en lugar de navy
|
||||
|
||||
---
|
||||
|
||||
## Responsive Behavior
|
||||
|
||||
### Desktop (≥992px)
|
||||
- Grid de 2 columnas activo
|
||||
- Header con layout horizontal
|
||||
- Espaciado completo
|
||||
|
||||
### Tablet (768px - 991px)
|
||||
- Stack vertical de columnas
|
||||
- Header con layout horizontal (flex-wrap si es necesario)
|
||||
- Espaciado completo
|
||||
|
||||
### Mobile (<768px)
|
||||
- Stack vertical completo
|
||||
- Header con botón debajo del título (flex-wrap)
|
||||
- Espaciado reducido
|
||||
|
||||
```css
|
||||
@media (max-width: 991px) {
|
||||
.tab-header {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.tab-header h3 {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.tab-header h3 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 0.75rem !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Volver al Índice
|
||||
|
||||
[← Volver al README](README.md)
|
||||
@@ -0,0 +1,92 @@
|
||||
# 🧩 COMPONENTES REUTILIZABLES
|
||||
|
||||
## ¿Qué es esto?
|
||||
|
||||
Una **librería de 28 componentes HTML** listos para copiar y pegar, construidos con Bootstrap 5.3.2 y alineados al design system de APU.
|
||||
|
||||
Cada componente incluye:
|
||||
- ✅ HTML completo y funcional
|
||||
- ✅ Múltiples ejemplos de uso
|
||||
- ✅ Comentarios explicativos inline
|
||||
- ✅ Fixes para conflictos con WordPress
|
||||
|
||||
---
|
||||
|
||||
## 📂 Documentación Completa
|
||||
|
||||
**Ir a:** [`../componentes-html-bootstrap/README.md`](../componentes-html-bootstrap/README.md)
|
||||
|
||||
Ahí encontrarás:
|
||||
- Índice completo de los 28 componentes
|
||||
- Categorías organizadas (formularios, botones, layouts, navegación, etc.)
|
||||
- Instrucciones de uso y personalización
|
||||
- Dependencias requeridas (CDN links)
|
||||
- Fixes CSS para WordPress
|
||||
- Componentes que requieren JavaScript
|
||||
|
||||
---
|
||||
|
||||
## 📋 Categorías de Componentes
|
||||
|
||||
### 🎨 Layout y Estructura
|
||||
Headers con gradiente, Cards (estándar y vista previa), Grids especializados
|
||||
|
||||
### 📝 Formularios
|
||||
Switches, Color pickers, Text inputs, Textareas, Selects, Radio buttons, Checkboxes, Range sliders, File uploads
|
||||
|
||||
### 🔘 Botones
|
||||
Botones simples, Button groups (Desktop/Mobile), Botones con dropdowns
|
||||
|
||||
### 🎯 Componentes Visuales
|
||||
Badges, Links externos, Progress bars, Alerts, Spinners, Dividers, Tooltips
|
||||
|
||||
### 📱 Navegación y Organización
|
||||
Tabs, Accordions, Sortable lists
|
||||
|
||||
### 💻 Desarrollo y Utilidades
|
||||
Image previews, Code blocks
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Cómo Usar
|
||||
|
||||
1. Abre el README de componentes: [`../componentes-html-bootstrap/README.md`](../componentes-html-bootstrap/README.md)
|
||||
2. Encuentra el componente que necesitas
|
||||
3. Abre el archivo HTML correspondiente (ej: `05-COLOR-PICKER.html`)
|
||||
4. Copia el HTML del ejemplo que te sirva
|
||||
5. Pega en tu proyecto y personaliza IDs/textos
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Principios de Uso
|
||||
|
||||
### Colores
|
||||
- **Iconos:** Siempre orange (`#FF8600`)
|
||||
- **Títulos de card:** Siempre navy (`#1e3a5f`)
|
||||
- **Border-left cards:** Navy para estándar, Orange para previews
|
||||
- **Botones primarios:** Orange
|
||||
|
||||
### Tamaños
|
||||
- **Form controls:** Usar `.form-control-sm` (tamaño compacto)
|
||||
- **Botones en header:** Usar `.btn-sm`
|
||||
- **Labels:** Clase `.small` + `fw-semibold`
|
||||
|
||||
### Patrones
|
||||
- **Header de tab:** Gradiente navy + border-left orange + botón reset derecha
|
||||
- **Vista previa:** Card con border-left orange + título "Vista Previa en Tiempo Real"
|
||||
- **Iconos en labels:** `<i class="bi bi-[icon] me-1" style="color: #FF8600;"></i>`
|
||||
|
||||
---
|
||||
|
||||
## 📝 Notas Importantes
|
||||
|
||||
1. Los componentes HTML son la **fuente de verdad** - siempre usa el código de ahí
|
||||
2. Todos los componentes están probados en WordPress
|
||||
3. No mezclar inline styles con clases de Bootstrap (solo usa inline styles para colores de marca)
|
||||
4. Siempre mantener accesibilidad (ARIA attributes incluidos)
|
||||
|
||||
---
|
||||
|
||||
## Volver al Índice
|
||||
|
||||
[← Volver al README](README.md)
|
||||
443
_planificacion/01-design-system/08-PATRONES-FORMULARIOS.md
Normal file
443
_planificacion/01-design-system/08-PATRONES-FORMULARIOS.md
Normal file
@@ -0,0 +1,443 @@
|
||||
# 📝 PATRONES DE FORMULARIOS
|
||||
|
||||
## 1. Form Switch (Checkbox Toggle)
|
||||
|
||||
```html
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="enabled" checked>
|
||||
<label class="form-check-label small" for="enabled" style="color: #495057;">
|
||||
<i class="bi bi-toggle-on me-1" style="color: #FF8600;"></i>
|
||||
<strong>Activar Top Bar</strong>
|
||||
<span class="text-muted">(Mostrar en el sitio)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Uso:** Activación/desactivación de features
|
||||
|
||||
**CSS necesario (Fix WordPress):**
|
||||
```css
|
||||
/* Eliminar pseudo-elementos de WordPress */
|
||||
body .form-switch .form-check-input[type="checkbox"]::before,
|
||||
body .form-switch .form-check-input[type="checkbox"]::after {
|
||||
content: none !important;
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body .form-switch .form-check-input[type="checkbox"] {
|
||||
background-size: contain !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-position: left center !important;
|
||||
}
|
||||
|
||||
body .form-switch .form-check-input[type="checkbox"]:checked {
|
||||
background-position: right center !important;
|
||||
}
|
||||
|
||||
/* Fix alineación vertical */
|
||||
.form-check.form-switch {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.form-switch .form-check-input {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.form-switch .form-check-label {
|
||||
line-height: 16px !important;
|
||||
padding-top: 0 !important;
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Color Picker
|
||||
|
||||
```html
|
||||
<div class="col-6">
|
||||
<label for="bgColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-paint-bucket me-1" style="color: #FF8600;"></i>
|
||||
Color de Fondo
|
||||
</label>
|
||||
<input type="color" id="bgColor"
|
||||
class="form-control form-control-color w-100"
|
||||
value="#0E2337"
|
||||
title="Seleccionar color de fondo">
|
||||
<small class="text-muted d-block mt-1" id="bgColorValue">#0E2337</small>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Características:**
|
||||
- Grid de 2 columnas (`col-6`)
|
||||
- Display del valor hex debajo
|
||||
- Width 100% (`.w-100`)
|
||||
|
||||
**JavaScript:**
|
||||
```javascript
|
||||
const colorInput = document.getElementById('bgColor');
|
||||
const colorValue = document.getElementById('bgColorValue');
|
||||
|
||||
colorInput.addEventListener('input', function() {
|
||||
colorValue.textContent = this.value.toUpperCase();
|
||||
updatePreview();
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Text Input con Icono
|
||||
|
||||
```html
|
||||
<div class="mb-2">
|
||||
<label for="highlightText" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-chat-text me-1" style="color: #FF8600;"></i>
|
||||
Texto Destacado
|
||||
<span class="text-danger">*</span> <!-- Si es requerido -->
|
||||
</label>
|
||||
<input type="text" id="highlightText"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="Ej: Nuevo:"
|
||||
value="Nuevo:"
|
||||
maxlength="50">
|
||||
</div>
|
||||
```
|
||||
|
||||
**Uso:** Campos de texto cortos (nombres, títulos, etc.)
|
||||
|
||||
---
|
||||
|
||||
## 4. Textarea con Contador y Progress Bar
|
||||
|
||||
```html
|
||||
<div class="mb-2">
|
||||
<label for="messageText" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-chat-left-text me-1" style="color: #FF8600;"></i>
|
||||
Mensaje Principal <span class="text-danger">*</span>
|
||||
<span class="float-end text-muted">
|
||||
<span id="messageTextCount" class="fw-bold">0</span>/250
|
||||
</span>
|
||||
</label>
|
||||
<textarea id="messageText"
|
||||
class="form-control form-control-sm"
|
||||
rows="2"
|
||||
maxlength="250"
|
||||
placeholder="Ej: Accede a más de 200,000 APUs actualizados"
|
||||
required></textarea>
|
||||
<div class="progress mt-1" style="height: 3px;">
|
||||
<div id="messageTextProgress"
|
||||
class="progress-bar"
|
||||
role="progressbar"
|
||||
style="width: 0%; background-color: #FF8600;"
|
||||
aria-valuenow="0"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="250"></div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**JavaScript:**
|
||||
```javascript
|
||||
const textarea = document.getElementById('messageText');
|
||||
const counter = document.getElementById('messageTextCount');
|
||||
const progress = document.getElementById('messageTextProgress');
|
||||
|
||||
textarea.addEventListener('input', function() {
|
||||
const length = this.value.length;
|
||||
const maxLength = 250;
|
||||
const percentage = (length / maxLength) * 100;
|
||||
|
||||
counter.textContent = length;
|
||||
progress.style.width = percentage + '%';
|
||||
progress.setAttribute('aria-valuenow', length);
|
||||
|
||||
// Cambiar color según el uso
|
||||
if (percentage > 90) {
|
||||
progress.style.backgroundColor = '#dc3545'; // Rojo
|
||||
} else if (percentage > 75) {
|
||||
progress.style.backgroundColor = '#ffc107'; // Amarillo
|
||||
} else {
|
||||
progress.style.backgroundColor = '#FF8600'; // Orange
|
||||
}
|
||||
|
||||
updatePreview();
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Select Dropdown
|
||||
|
||||
```html
|
||||
<div class="mb-2">
|
||||
<label for="fontSize" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-fonts me-1" style="color: #FF8600;"></i>
|
||||
Tamaño de Fuente
|
||||
</label>
|
||||
<select id="fontSize" class="form-select form-select-sm">
|
||||
<option value="small">Pequeño (0.875rem)</option>
|
||||
<option value="normal" selected>Normal (1rem)</option>
|
||||
<option value="large">Grande (1.125rem)</option>
|
||||
</select>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Uso:** Opciones predefinidas (tamaños, estilos, etc.)
|
||||
|
||||
---
|
||||
|
||||
## 6. Badges Informativos
|
||||
|
||||
### Badge en Label
|
||||
|
||||
```html
|
||||
<label class="form-label small mb-1 fw-semibold">
|
||||
<i class="bi bi-code me-1" style="color: #FF8600;"></i>
|
||||
Clase del Icono
|
||||
<span class="badge bg-secondary" style="font-size: 0.65rem;">Bootstrap Icons</span>
|
||||
</label>
|
||||
```
|
||||
|
||||
### Badge Standalone
|
||||
|
||||
```html
|
||||
<!-- Opcional -->
|
||||
<span class="badge text-dark" style="background-color: #FFB800; font-size: 0.65rem;">
|
||||
Opcional
|
||||
</span>
|
||||
|
||||
<!-- Info -->
|
||||
<span class="badge bg-secondary" style="font-size: 0.65rem;">
|
||||
Info
|
||||
</span>
|
||||
|
||||
<!-- Requerido -->
|
||||
<span class="badge bg-danger" style="font-size: 0.65rem;">
|
||||
Requerido
|
||||
</span>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Links de Ayuda
|
||||
|
||||
```html
|
||||
<small class="text-muted d-block mt-1">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Ver: <a href="https://icons.getbootstrap.com/" target="_blank"
|
||||
class="text-decoration-none" style="color: #FF8600;">
|
||||
Bootstrap Icons <i class="bi bi-box-arrow-up-right"></i>
|
||||
</a>
|
||||
</small>
|
||||
```
|
||||
|
||||
**Características:**
|
||||
- Icono de info
|
||||
- Link orange con hover
|
||||
- Icono de "abrir en nueva ventana"
|
||||
- `target="_blank"` para abrir en pestaña nueva
|
||||
|
||||
---
|
||||
|
||||
## 8. Grid de Inputs Compactos
|
||||
|
||||
### 2 Columnas Iguales
|
||||
|
||||
```html
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="bgColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-paint-bucket me-1" style="color: #FF8600;"></i>
|
||||
Color Fondo
|
||||
</label>
|
||||
<input type="color" id="bgColor"
|
||||
class="form-control form-control-color w-100"
|
||||
value="#0E2337">
|
||||
<small class="text-muted d-block mt-1" id="bgColorValue">#0E2337</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="textColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-fonts me-1" style="color: #FF8600;"></i>
|
||||
Color Texto
|
||||
</label>
|
||||
<input type="color" id="textColor"
|
||||
class="form-control form-control-color w-100"
|
||||
value="#ffffff">
|
||||
<small class="text-muted d-block mt-1" id="textColorValue">#FFFFFF</small>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 3 Columnas Desiguales
|
||||
|
||||
```html
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-5">
|
||||
<label class="form-label small mb-1 fw-semibold">Campo 1</label>
|
||||
<input type="text" class="form-control form-control-sm">
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<label class="form-label small mb-1 fw-semibold">Campo 2</label>
|
||||
<input type="text" class="form-control form-control-sm">
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<label class="form-label small mb-1 fw-semibold">Icono</label>
|
||||
<input type="text" class="form-control form-control-sm">
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Campo de URL
|
||||
|
||||
```html
|
||||
<div class="mb-2">
|
||||
<label for="linkUrl" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-link-45deg me-1" style="color: #FF8600;"></i>
|
||||
URL del Enlace
|
||||
</label>
|
||||
<input type="url" id="linkUrl"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="https://ejemplo.com"
|
||||
value="/catalogo">
|
||||
<small class="text-muted d-block mt-1">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Usa rutas relativas (/) o absolutas (https://)
|
||||
</small>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Campos Relacionados (Link)
|
||||
|
||||
```html
|
||||
<!-- Switch para mostrar/ocultar link -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="showLink" checked>
|
||||
<label class="form-check-label small" for="showLink" style="color: #495057;">
|
||||
<i class="bi bi-link me-1" style="color: #FF8600;"></i>
|
||||
<strong>Mostrar Enlace</strong>
|
||||
<span class="text-muted">(Call-to-action)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Campos del link (se muestran/ocultan según el switch) -->
|
||||
<div id="linkFields">
|
||||
<!-- Texto del link -->
|
||||
<div class="mb-2">
|
||||
<label for="linkText" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-chat-text me-1" style="color: #FF8600;"></i>
|
||||
Texto del Enlace
|
||||
</label>
|
||||
<input type="text" id="linkText"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="Ej: Ver Catálogo →"
|
||||
value="Ver Catálogo →"
|
||||
maxlength="50">
|
||||
</div>
|
||||
|
||||
<!-- URL del link -->
|
||||
<div class="mb-2">
|
||||
<label for="linkUrl" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-link-45deg me-1" style="color: #FF8600;"></i>
|
||||
URL del Enlace
|
||||
</label>
|
||||
<input type="url" id="linkUrl"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="https://ejemplo.com"
|
||||
value="/catalogo">
|
||||
</div>
|
||||
|
||||
<!-- Target del link -->
|
||||
<div class="mb-2">
|
||||
<label for="linkTarget" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-box-arrow-up-right me-1" style="color: #FF8600;"></i>
|
||||
Abrir En
|
||||
</label>
|
||||
<select id="linkTarget" class="form-select form-select-sm">
|
||||
<option value="_self" selected>Misma ventana (_self)</option>
|
||||
<option value="_blank">Nueva pestaña (_blank)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**JavaScript para mostrar/ocultar:**
|
||||
```javascript
|
||||
const showLink = document.getElementById('showLink');
|
||||
const linkFields = document.getElementById('linkFields');
|
||||
|
||||
showLink.addEventListener('change', function() {
|
||||
linkFields.style.display = this.checked ? 'block' : 'none';
|
||||
updatePreview();
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Validación de Campos
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Valida los campos del formulario
|
||||
*/
|
||||
function validateForm() {
|
||||
let isValid = true;
|
||||
const errors = [];
|
||||
|
||||
// Validar campo requerido
|
||||
const messageText = document.getElementById('messageText').value.trim();
|
||||
if (!messageText) {
|
||||
errors.push('El mensaje principal es requerido');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// Validar longitud
|
||||
if (messageText.length > 250) {
|
||||
errors.push('El mensaje no puede exceder 250 caracteres');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// Validar URL
|
||||
const linkUrl = document.getElementById('linkUrl').value.trim();
|
||||
if (linkUrl && !isValidUrl(linkUrl)) {
|
||||
errors.push('La URL no es válida');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
alert('⚠️ Errores de validación:\n\n' + errors.join('\n'));
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
function isValidUrl(string) {
|
||||
// Permitir rutas relativas
|
||||
if (string.startsWith('/')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Validar URLs absolutas
|
||||
try {
|
||||
new URL(string);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Volver al Índice
|
||||
|
||||
[← Volver al README](README.md)
|
||||
360
_planificacion/01-design-system/09-VISTA-PREVIA-TIEMPO-REAL.md
Normal file
360
_planificacion/01-design-system/09-VISTA-PREVIA-TIEMPO-REAL.md
Normal file
@@ -0,0 +1,360 @@
|
||||
# 👁️ VISTA PREVIA EN TIEMPO REAL
|
||||
|
||||
## Estructura HTML del Preview
|
||||
|
||||
```html
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #FF8600;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-eye me-2" style="color: #FF8600;"></i>
|
||||
Vista Previa en Tiempo Real
|
||||
</h5>
|
||||
|
||||
<!-- IMPORTANTE: Usar las MISMAS clases que el componente real -->
|
||||
<div id="componentPreview" class="[clases-del-componente-real]">
|
||||
<!-- HTML IDÉNTICO al del front-end -->
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||
<small class="text-muted">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Los cambios se reflejan en tiempo real
|
||||
</small>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button type="button" class="btn btn-outline-secondary active" id="previewDesktop">
|
||||
<i class="bi bi-display"></i> Desktop
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" id="previewMobile">
|
||||
<i class="bi bi-phone"></i> Mobile
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Características clave:**
|
||||
- ✅ Border-left ORANGE (#FF8600) - NO navy
|
||||
- ✅ Clases IDÉNTICAS al componente del front-end
|
||||
- ✅ HTML IDÉNTICO al front-end
|
||||
- ✅ Botones Desktop/Mobile para cambiar el ancho
|
||||
|
||||
---
|
||||
|
||||
## Reglas Críticas para Vista Previa
|
||||
|
||||
### ❌ LO QUE NO DEBES HACER
|
||||
|
||||
#### 1. NO usar inline styles que sobreescriban el CSS real
|
||||
|
||||
```html
|
||||
<!-- ❌ MAL: Inline styles que sobreescriben el CSS real -->
|
||||
<div id="preview" class="component" style="padding: 10px; color: blue;">
|
||||
<!-- Esto NO se verá igual al front-end -->
|
||||
</div>
|
||||
```
|
||||
|
||||
#### 2. NO crear CSS específico para el preview que no existe en el front-end
|
||||
|
||||
```css
|
||||
/* ❌ MAL: CSS específico para el preview que no existe en el front-end */
|
||||
#componentPreview {
|
||||
padding: 20px;
|
||||
font-size: 16px;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. NO modificar la estructura HTML del componente
|
||||
|
||||
```html
|
||||
<!-- ❌ MAL: HTML diferente al front-end -->
|
||||
<div id="preview" class="my-custom-wrapper">
|
||||
<div class="component">
|
||||
<!-- Estructura diferente -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ✅ LO QUE DEBES HACER
|
||||
|
||||
#### 1. Usar EXACTAMENTE las mismas clases que el front-end
|
||||
|
||||
```html
|
||||
<!-- ✅ BIEN: Usar EXACTAMENTE las mismas clases que el front-end -->
|
||||
<div id="topBarPreview" class="top-notification-bar">
|
||||
<div class="container">
|
||||
<div class="notification-content">
|
||||
<!-- Estructura IDÉNTICA al front-end -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### 2. Cargar el CSS del front-end en el admin
|
||||
|
||||
```html
|
||||
<!-- ✅ BIEN: Cargar el CSS real del front-end -->
|
||||
<link rel="stylesheet" href="../../Css/style.css">
|
||||
```
|
||||
|
||||
#### 3. Solo aplicar estilos si es ABSOLUTAMENTE necesario con !important
|
||||
|
||||
```css
|
||||
/* ✅ BIEN: Solo si el CSS del front-end no se aplica correctamente */
|
||||
#componentPreview.top-notification-bar {
|
||||
/* Solo agregar si es necesario para el contexto del admin */
|
||||
border: 1px solid #e9ecef; /* Border para debugging visual */
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ejemplo Completo: Top Bar Preview
|
||||
|
||||
### HTML del Front-end (Original)
|
||||
|
||||
```html
|
||||
<div class="top-notification-bar">
|
||||
<div class="container">
|
||||
<div class="notification-content">
|
||||
<i class="bi bi-megaphone-fill notification-icon"></i>
|
||||
<span class="notification-text">
|
||||
<strong class="notification-highlight">Nuevo:</strong>
|
||||
Accede a más de 200,000 APUs actualizados.
|
||||
</span>
|
||||
<a href="/catalogo" class="notification-link">
|
||||
Ver Catálogo →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### HTML del Preview (Admin) ✅ CORRECTO
|
||||
|
||||
```html
|
||||
<!-- ✅ IDÉNTICO al front-end -->
|
||||
<div id="topBarPreview" class="top-notification-bar">
|
||||
<div class="container">
|
||||
<div class="notification-content">
|
||||
<i class="bi bi-megaphone-fill notification-icon" id="previewIcon"></i>
|
||||
<span class="notification-text">
|
||||
<strong class="notification-highlight" id="previewHighlight">Nuevo:</strong>
|
||||
<span id="previewMessage">Accede a más de 200,000 APUs actualizados.</span>
|
||||
</span>
|
||||
<a href="/catalogo" class="notification-link" id="previewLink">
|
||||
Ver Catálogo →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Diferencias permitidas:**
|
||||
- ✅ IDs agregados para manipulación con JavaScript (`id="previewIcon"`, `id="previewMessage"`, etc.)
|
||||
- ✅ Clases CSS IDÉNTICAS al front-end
|
||||
- ✅ Estructura HTML IDÉNTICA
|
||||
|
||||
---
|
||||
|
||||
## CSS para Vista Previa
|
||||
|
||||
### Principio: NO sobreescribir estilos del front-end
|
||||
|
||||
```css
|
||||
/* REGLA DE ORO: NO sobreescribir estilos del front-end */
|
||||
/* Solo agregar si es absolutamente necesario */
|
||||
|
||||
/* ✅ Permitido: Border para distinguir visualmente el preview en el admin */
|
||||
#topBarPreview {
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
/* ❌ NO permitido: Sobreescribir propiedades del componente */
|
||||
#topBarPreview {
|
||||
padding: 20px !important; /* ❌ Esto hará que no se vea igual */
|
||||
font-size: 18px !important; /* ❌ Esto hará que no se vea igual */
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## JavaScript para updatePreview()
|
||||
|
||||
### Patrón Básico
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Actualiza la vista previa en tiempo real
|
||||
*/
|
||||
function updatePreview() {
|
||||
const preview = document.getElementById('topBarPreview');
|
||||
if (!preview) return;
|
||||
|
||||
// REGLA: Solo modificar propiedades que el usuario puede cambiar
|
||||
// NO modificar padding, margins, o estructura del HTML
|
||||
|
||||
// 1. Actualizar colores
|
||||
const bgColor = document.getElementById('bgColor').value;
|
||||
const textColor = document.getElementById('textColor').value;
|
||||
preview.style.backgroundColor = bgColor;
|
||||
preview.style.color = textColor;
|
||||
|
||||
// 2. Actualizar texto
|
||||
const highlightText = document.getElementById('highlightText').value;
|
||||
const messageText = document.getElementById('messageText').value;
|
||||
document.getElementById('previewHighlight').textContent = highlightText;
|
||||
document.getElementById('previewMessage').textContent = messageText;
|
||||
|
||||
// 3. Actualizar link
|
||||
const showLink = document.getElementById('showLink').checked;
|
||||
const linkElement = document.getElementById('previewLink');
|
||||
linkElement.style.display = showLink ? 'inline-block' : 'none';
|
||||
|
||||
if (showLink) {
|
||||
const linkText = document.getElementById('linkText').value;
|
||||
const linkUrl = document.getElementById('linkUrl').value;
|
||||
const linkTarget = document.getElementById('linkTarget').value;
|
||||
linkElement.textContent = linkText;
|
||||
linkElement.href = linkUrl;
|
||||
linkElement.target = linkTarget;
|
||||
}
|
||||
|
||||
// 4. Mostrar/ocultar icono
|
||||
const showIcon = document.getElementById('showIcon').checked;
|
||||
const iconElement = document.getElementById('previewIcon');
|
||||
iconElement.style.display = showIcon ? 'inline-block' : 'none';
|
||||
|
||||
// 5. Cambiar clase del icono
|
||||
const iconClass = document.getElementById('iconClass').value;
|
||||
iconElement.className = iconClass + ' notification-icon';
|
||||
}
|
||||
```
|
||||
|
||||
### Conectar updatePreview() a Todos los Campos
|
||||
|
||||
```javascript
|
||||
function initializeEventListeners() {
|
||||
// Lista de campos que deben actualizar el preview
|
||||
const fields = [
|
||||
'bgColor',
|
||||
'textColor',
|
||||
'highlightColor',
|
||||
'highlightText',
|
||||
'messageText',
|
||||
'showLink',
|
||||
'linkText',
|
||||
'linkUrl',
|
||||
'linkTarget',
|
||||
'showIcon',
|
||||
'iconClass',
|
||||
'fontSize'
|
||||
];
|
||||
|
||||
// Conectar event listeners
|
||||
fields.forEach(fieldId => {
|
||||
const element = document.getElementById(fieldId);
|
||||
if (element) {
|
||||
if (element.type === 'checkbox') {
|
||||
element.addEventListener('change', updatePreview);
|
||||
} else {
|
||||
element.addEventListener('input', updatePreview);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Botones Desktop/Mobile
|
||||
|
||||
### HTML
|
||||
|
||||
```html
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button type="button" class="btn btn-outline-secondary active" id="previewDesktop">
|
||||
<i class="bi bi-display"></i> Desktop
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" id="previewMobile">
|
||||
<i class="bi bi-phone"></i> Mobile
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
### JavaScript
|
||||
|
||||
```javascript
|
||||
const btnDesktop = document.getElementById('previewDesktop');
|
||||
const btnMobile = document.getElementById('previewMobile');
|
||||
const preview = document.getElementById('topBarPreview');
|
||||
|
||||
btnDesktop.addEventListener('click', function() {
|
||||
// Activar botón
|
||||
this.classList.add('active');
|
||||
btnMobile.classList.remove('active');
|
||||
|
||||
// Cambiar ancho del preview
|
||||
preview.style.maxWidth = '100%';
|
||||
preview.style.margin = '0';
|
||||
});
|
||||
|
||||
btnMobile.addEventListener('click', function() {
|
||||
// Activar botón
|
||||
this.classList.add('active');
|
||||
btnDesktop.classList.remove('active');
|
||||
|
||||
// Cambiar ancho del preview (simular mobile)
|
||||
preview.style.maxWidth = '375px';
|
||||
preview.style.margin = '0 auto';
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checklist de Vista Previa
|
||||
|
||||
Antes de considerar completa la vista previa, verificar:
|
||||
|
||||
- [ ] HTML del preview es IDÉNTICO al front-end
|
||||
- [ ] Se usan las MISMAS clases CSS que el componente real
|
||||
- [ ] Se carga el archivo CSS del front-end (`../../Css/style.css`)
|
||||
- [ ] NO hay inline styles que sobreescriban propiedades del componente
|
||||
- [ ] La función `updatePreview()` está conectada a todos los campos
|
||||
- [ ] Los botones Desktop/Mobile funcionan correctamente
|
||||
- [ ] El preview se ve IDÉNTICO al componente en el sitio real
|
||||
|
||||
---
|
||||
|
||||
## Debugging de Vista Previa
|
||||
|
||||
### Problema: El preview no se ve igual al front-end
|
||||
|
||||
**Solución:**
|
||||
|
||||
1. Abrir DevTools (F12)
|
||||
2. Inspeccionar el elemento del preview
|
||||
3. Verificar que se cargan los estilos correctos:
|
||||
|
||||
```
|
||||
Computed Styles →
|
||||
padding: 0.5rem 0 (debe venir de style.css)
|
||||
background-color: rgb(14, 35, 55) (debe venir del inline style del preview)
|
||||
```
|
||||
|
||||
4. Si hay estilos incorrectos:
|
||||
- Verificar que `style.css` esté cargado
|
||||
- Verificar que las clases CSS sean idénticas
|
||||
- Verificar que no haya inline styles conflictivos
|
||||
|
||||
---
|
||||
|
||||
## Volver al Índice
|
||||
|
||||
[← Volver al README](README.md)
|
||||
422
_planificacion/01-design-system/10-RESPONSIVE-DESIGN.md
Normal file
422
_planificacion/01-design-system/10-RESPONSIVE-DESIGN.md
Normal file
@@ -0,0 +1,422 @@
|
||||
# 📱 RESPONSIVE DESIGN
|
||||
|
||||
## Breakpoints de Bootstrap
|
||||
|
||||
| Breakpoint | Min Width | Dispositivo | Uso en Admin Panel |
|
||||
|------------|-----------|-------------|-------------------|
|
||||
| **xs** | 0px | Móvil pequeño | Stack vertical, padding reducido |
|
||||
| **sm** | 576px | Móvil grande | Stack vertical |
|
||||
| **md** | 768px | Tablet | Stack vertical |
|
||||
| **lg** | 992px | Desktop | Grid 2 columnas activo |
|
||||
| **xl** | 1200px | Desktop grande | Grid 2 columnas |
|
||||
| **xxl** | 1400px | Desktop XL | Grid 2 columnas |
|
||||
|
||||
---
|
||||
|
||||
## Media Queries
|
||||
|
||||
### Mobile (<576px)
|
||||
|
||||
```css
|
||||
@media (max-width: 575px) {
|
||||
/* Header del Tab: reducir tamaño de título */
|
||||
.tab-header h3 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Cards: reducir padding */
|
||||
.form-section.card .card-body {
|
||||
padding: 0.75rem !important;
|
||||
}
|
||||
|
||||
/* Container: reducir padding lateral */
|
||||
.container-fluid {
|
||||
padding-right: 0.5rem;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
/* Botones: full width */
|
||||
.tab-header button {
|
||||
width: 100%;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tablet (576px - 991px)
|
||||
|
||||
```css
|
||||
@media (max-width: 991px) {
|
||||
/* Header del Tab: reducir padding */
|
||||
.tab-header {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
/* Título: tamaño medio */
|
||||
.tab-header h3 {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Grid: todavía en stack vertical */
|
||||
}
|
||||
```
|
||||
|
||||
### Desktop (≥992px)
|
||||
|
||||
```css
|
||||
@media (min-width: 992px) {
|
||||
/* Grid de 2 columnas activo */
|
||||
.col-lg-6 {
|
||||
flex: 0 0 auto;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
/* Espaciado normal */
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Patrones Responsive
|
||||
|
||||
### Header del Tab
|
||||
|
||||
#### HTML Responsive
|
||||
|
||||
```html
|
||||
<div class="rounded p-4 mb-4 shadow text-white"
|
||||
style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%);">
|
||||
<!-- flex-wrap permite que el botón baje en mobile -->
|
||||
<!-- gap-3 mantiene espaciado consistente -->
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
|
||||
<div>
|
||||
<h3 class="h4 mb-1 fw-bold">Título</h3>
|
||||
<p class="mb-0 small">Descripción</p>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-light">Botón</button>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### CSS Responsive
|
||||
|
||||
```css
|
||||
@media (max-width: 576px) {
|
||||
/* El botón baja automáticamente con flex-wrap */
|
||||
/* gap-3 mantiene el espaciado */
|
||||
|
||||
/* Opcional: hacer botón full-width */
|
||||
.tab-header button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Reducir padding del header */
|
||||
.tab-header {
|
||||
padding: 0.75rem !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Grid de 2 Columnas
|
||||
|
||||
#### HTML Responsive
|
||||
|
||||
```html
|
||||
<div class="row g-3">
|
||||
<!-- col-lg-6: 2 columnas en desktop (≥992px) -->
|
||||
<!-- Stack vertical automático en mobile/tablet (<992px) -->
|
||||
<div class="col-lg-6">
|
||||
<!-- Cards de configuración -->
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<!-- Cards de configuración -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Comportamiento:**
|
||||
- **Desktop (≥992px)**: 2 columnas lado a lado
|
||||
- **Mobile/Tablet (<992px)**: Stack vertical automático
|
||||
- **Gap**: 1rem (16px) entre elementos
|
||||
|
||||
---
|
||||
|
||||
### Botones de Vista Previa
|
||||
|
||||
#### HTML Responsive
|
||||
|
||||
```html
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button type="button" class="btn btn-outline-secondary active">
|
||||
<i class="bi bi-display"></i> Desktop
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-phone"></i> Mobile
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### CSS Responsive
|
||||
|
||||
```css
|
||||
@media (max-width: 576px) {
|
||||
/* Reducir tamaño de botones en mobile */
|
||||
.btn-group.btn-group-sm .btn {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
/* Opcional: full width */
|
||||
.btn-group.btn-group-sm {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Cards
|
||||
|
||||
#### HTML Responsive (Automático)
|
||||
|
||||
```html
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<!-- Contenido -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### CSS Responsive
|
||||
|
||||
```css
|
||||
@media (max-width: 575px) {
|
||||
/* Reducir padding en cards */
|
||||
.card-body {
|
||||
padding: 0.75rem !important;
|
||||
}
|
||||
|
||||
/* Reducir margen entre cards */
|
||||
.card {
|
||||
margin-bottom: 0.75rem !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Contenedor Principal
|
||||
|
||||
### HTML
|
||||
|
||||
```html
|
||||
<div class="container-fluid py-4" style="max-width: 1400px;">
|
||||
<!-- Contenido -->
|
||||
</div>
|
||||
```
|
||||
|
||||
### CSS Responsive
|
||||
|
||||
```css
|
||||
/* Desktop: ancho máximo 1400px */
|
||||
@media (min-width: 1400px) {
|
||||
.container-fluid {
|
||||
max-width: 1400px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablet: ancho completo con padding */
|
||||
@media (max-width: 991px) {
|
||||
.container-fluid {
|
||||
padding-right: 1rem;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile: padding reducido */
|
||||
@media (max-width: 575px) {
|
||||
.container-fluid {
|
||||
padding-right: 0.5rem;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Vista Previa Responsive
|
||||
|
||||
### Simular Mobile en Preview
|
||||
|
||||
```javascript
|
||||
const btnMobile = document.getElementById('previewMobile');
|
||||
const preview = document.getElementById('componentPreview');
|
||||
|
||||
btnMobile.addEventListener('click', function() {
|
||||
// Simular ancho mobile (375px - iPhone SE)
|
||||
preview.style.maxWidth = '375px';
|
||||
preview.style.margin = '0 auto';
|
||||
|
||||
// Opcional: agregar border para visualizar límites
|
||||
preview.style.border = '1px solid #e9ecef';
|
||||
});
|
||||
```
|
||||
|
||||
### Simular Desktop en Preview
|
||||
|
||||
```javascript
|
||||
const btnDesktop = document.getElementById('previewDesktop');
|
||||
const preview = document.getElementById('componentPreview');
|
||||
|
||||
btnDesktop.addEventListener('click', function() {
|
||||
// Restaurar ancho completo
|
||||
preview.style.maxWidth = '100%';
|
||||
preview.style.margin = '0';
|
||||
|
||||
// Remover border
|
||||
preview.style.border = 'none';
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Anchos de Referencia para Dispositivos
|
||||
|
||||
| Dispositivo | Ancho (px) | Uso en Preview |
|
||||
|-------------|------------|----------------|
|
||||
| iPhone SE | 375 | Mobile pequeño |
|
||||
| iPhone 12/13 | 390 | Mobile estándar |
|
||||
| iPhone 14 Pro Max | 430 | Mobile grande |
|
||||
| iPad Mini | 768 | Tablet |
|
||||
| iPad Pro | 1024 | Tablet grande |
|
||||
| Desktop | 1200+ | Desktop estándar |
|
||||
|
||||
---
|
||||
|
||||
## Tipografía Responsive
|
||||
|
||||
### Headers
|
||||
|
||||
```css
|
||||
/* Desktop: tamaño completo */
|
||||
@media (min-width: 992px) {
|
||||
.tab-header h3 {
|
||||
font-size: 1.5rem; /* 24px */
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1rem; /* 16px */
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablet: tamaño reducido */
|
||||
@media (max-width: 991px) {
|
||||
.tab-header h3 {
|
||||
font-size: 1.1rem; /* 17.6px */
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile: tamaño mínimo */
|
||||
@media (max-width: 575px) {
|
||||
.tab-header h3 {
|
||||
font-size: 1rem; /* 16px */
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 0.9rem; /* 14.4px */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Espaciado Responsive
|
||||
|
||||
### Padding
|
||||
|
||||
```css
|
||||
/* Desktop: padding completo */
|
||||
@media (min-width: 992px) {
|
||||
.tab-header {
|
||||
padding: 1.5rem; /* 24px */
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1rem; /* 16px */
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile: padding reducido */
|
||||
@media (max-width: 575px) {
|
||||
.tab-header {
|
||||
padding: 0.75rem !important; /* 12px */
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 0.75rem !important; /* 12px */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Utilidades de Bootstrap para Responsive
|
||||
|
||||
### Display
|
||||
|
||||
```html
|
||||
<!-- Ocultar en mobile, mostrar en desktop -->
|
||||
<div class="d-none d-lg-block">Solo en desktop</div>
|
||||
|
||||
<!-- Mostrar en mobile, ocultar en desktop -->
|
||||
<div class="d-lg-none">Solo en mobile</div>
|
||||
|
||||
<!-- Mostrar en todos los tamaños -->
|
||||
<div class="d-block">Siempre visible</div>
|
||||
```
|
||||
|
||||
### Flex Direction
|
||||
|
||||
```html
|
||||
<!-- Columna en mobile, fila en desktop -->
|
||||
<div class="d-flex flex-column flex-lg-row">
|
||||
<div>Item 1</div>
|
||||
<div>Item 2</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Text Alignment
|
||||
|
||||
```html
|
||||
<!-- Centrado en mobile, izquierda en desktop -->
|
||||
<p class="text-center text-lg-start">Texto</p>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Responsive
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] Probar en mobile (<576px)
|
||||
- [ ] Probar en tablet (768px)
|
||||
- [ ] Probar en desktop (≥992px)
|
||||
- [ ] Header del tab se adapta correctamente
|
||||
- [ ] Grid de 2 columnas se convierte en stack vertical
|
||||
- [ ] Cards mantienen legibilidad
|
||||
- [ ] Botones no se cortan
|
||||
- [ ] Vista previa funciona en todos los tamaños
|
||||
|
||||
### Herramientas
|
||||
|
||||
1. **Chrome DevTools**: F12 → Toggle Device Toolbar (Ctrl+Shift+M)
|
||||
2. **Firefox Responsive Design Mode**: F12 → Toggle Responsive Design Mode
|
||||
3. **Dispositivos reales**: Probar en iPhone, iPad, Android
|
||||
|
||||
---
|
||||
|
||||
## Volver al Índice
|
||||
|
||||
[← Volver al README](README.md)
|
||||
548
_planificacion/01-design-system/11-JAVASCRIPT-PATTERNS.md
Normal file
548
_planificacion/01-design-system/11-JAVASCRIPT-PATTERNS.md
Normal file
@@ -0,0 +1,548 @@
|
||||
# 💻 JAVASCRIPT PATTERNS
|
||||
|
||||
## 1. Inicialización del Componente
|
||||
|
||||
```javascript
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('✅ [ComponentName] Admin Panel cargado');
|
||||
|
||||
// 1. Cargar configuración guardada
|
||||
loadConfig();
|
||||
|
||||
// 2. Inicializar vista previa
|
||||
updatePreview();
|
||||
|
||||
// 3. Conectar event listeners
|
||||
initializeEventListeners();
|
||||
});
|
||||
```
|
||||
|
||||
**Orden de ejecución:**
|
||||
1. `loadConfig()`: Carga valores guardados desde localStorage/JSON
|
||||
2. `updatePreview()`: Renderiza el preview inicial
|
||||
3. `initializeEventListeners()`: Conecta los campos al preview
|
||||
|
||||
---
|
||||
|
||||
## 2. Event Listeners
|
||||
|
||||
### Patrón Básico
|
||||
|
||||
```javascript
|
||||
function initializeEventListeners() {
|
||||
// Lista de campos que deben actualizar el preview
|
||||
const fields = [
|
||||
'enabled',
|
||||
'showOnMobile',
|
||||
'bgColor',
|
||||
'textColor',
|
||||
'highlightText',
|
||||
'messageText',
|
||||
'showLink',
|
||||
'linkText',
|
||||
'linkUrl'
|
||||
];
|
||||
|
||||
// Conectar event listeners automáticamente
|
||||
fields.forEach(fieldId => {
|
||||
const element = document.getElementById(fieldId);
|
||||
if (element) {
|
||||
// Checkboxes: usar 'change'
|
||||
if (element.type === 'checkbox') {
|
||||
element.addEventListener('change', updatePreview);
|
||||
}
|
||||
// Otros inputs: usar 'input' para tiempo real
|
||||
else {
|
||||
element.addEventListener('input', updatePreview);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Event listeners específicos
|
||||
initializeColorPickerListeners();
|
||||
initializeTextareaCounters();
|
||||
initializeResetButton();
|
||||
}
|
||||
```
|
||||
|
||||
### Color Pickers con Display Hex
|
||||
|
||||
```javascript
|
||||
function initializeColorPickerListeners() {
|
||||
const colorFields = [
|
||||
{ input: 'bgColor', display: 'bgColorValue' },
|
||||
{ input: 'textColor', display: 'textColorValue' },
|
||||
{ input: 'highlightColor', display: 'highlightColorValue' }
|
||||
];
|
||||
|
||||
colorFields.forEach(({ input, display }) => {
|
||||
const inputElement = document.getElementById(input);
|
||||
const displayElement = document.getElementById(display);
|
||||
|
||||
if (inputElement && displayElement) {
|
||||
inputElement.addEventListener('input', function() {
|
||||
displayElement.textContent = this.value.toUpperCase();
|
||||
updatePreview();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Textareas con Contadores
|
||||
|
||||
```javascript
|
||||
function initializeTextareaCounters() {
|
||||
const textareas = [
|
||||
{ field: 'messageText', counter: 'messageTextCount', progress: 'messageTextProgress', max: 250 },
|
||||
{ field: 'description', counter: 'descriptionCount', progress: 'descriptionProgress', max: 500 }
|
||||
];
|
||||
|
||||
textareas.forEach(({ field, counter, progress, max }) => {
|
||||
const textarea = document.getElementById(field);
|
||||
const counterElement = document.getElementById(counter);
|
||||
const progressElement = document.getElementById(progress);
|
||||
|
||||
if (textarea && counterElement && progressElement) {
|
||||
textarea.addEventListener('input', function() {
|
||||
const length = this.value.length;
|
||||
const percentage = (length / max) * 100;
|
||||
|
||||
// Actualizar contador
|
||||
counterElement.textContent = length;
|
||||
|
||||
// Actualizar progress bar
|
||||
progressElement.style.width = percentage + '%';
|
||||
progressElement.setAttribute('aria-valuenow', length);
|
||||
|
||||
// Cambiar color según uso
|
||||
if (percentage > 90) {
|
||||
progressElement.style.backgroundColor = '#dc3545'; // Rojo
|
||||
} else if (percentage > 75) {
|
||||
progressElement.style.backgroundColor = '#ffc107'; // Amarillo
|
||||
} else {
|
||||
progressElement.style.backgroundColor = '#FF8600'; // Orange
|
||||
}
|
||||
|
||||
updatePreview();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Botón de Reset
|
||||
|
||||
```javascript
|
||||
function initializeResetButton() {
|
||||
const resetBtn = document.getElementById('resetDefaults');
|
||||
if (resetBtn) {
|
||||
resetBtn.addEventListener('click', resetToDefaults);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Función updatePreview()
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Actualiza la vista previa en tiempo real
|
||||
* REGLA: Solo modificar propiedades que el usuario puede cambiar
|
||||
*/
|
||||
function updatePreview() {
|
||||
const preview = document.getElementById('componentPreview');
|
||||
if (!preview) return;
|
||||
|
||||
// 1. Activar/desactivar componente
|
||||
const enabled = document.getElementById('enabled').checked;
|
||||
preview.style.display = enabled ? 'block' : 'none';
|
||||
|
||||
// 2. Colores
|
||||
const bgColor = document.getElementById('bgColor').value;
|
||||
const textColor = document.getElementById('textColor').value;
|
||||
preview.style.backgroundColor = bgColor;
|
||||
preview.style.color = textColor;
|
||||
|
||||
// 3. Textos
|
||||
const highlightText = document.getElementById('highlightText').value;
|
||||
const messageText = document.getElementById('messageText').value;
|
||||
|
||||
const highlightElement = document.getElementById('previewHighlight');
|
||||
const messageElement = document.getElementById('previewMessage');
|
||||
|
||||
if (highlightElement) highlightElement.textContent = highlightText;
|
||||
if (messageElement) messageElement.textContent = messageText;
|
||||
|
||||
// 4. Mostrar/ocultar elementos
|
||||
const showIcon = document.getElementById('showIcon').checked;
|
||||
const iconElement = document.getElementById('previewIcon');
|
||||
if (iconElement) {
|
||||
iconElement.style.display = showIcon ? 'inline-block' : 'none';
|
||||
}
|
||||
|
||||
// 5. Cambiar clase del icono
|
||||
const iconClass = document.getElementById('iconClass').value;
|
||||
if (iconElement && iconClass) {
|
||||
iconElement.className = iconClass + ' notification-icon';
|
||||
}
|
||||
|
||||
// 6. Link
|
||||
const showLink = document.getElementById('showLink').checked;
|
||||
const linkElement = document.getElementById('previewLink');
|
||||
|
||||
if (linkElement) {
|
||||
linkElement.style.display = showLink ? 'inline-block' : 'none';
|
||||
|
||||
if (showLink) {
|
||||
const linkText = document.getElementById('linkText').value;
|
||||
const linkUrl = document.getElementById('linkUrl').value;
|
||||
const linkTarget = document.getElementById('linkTarget').value;
|
||||
|
||||
linkElement.textContent = linkText;
|
||||
linkElement.href = linkUrl;
|
||||
linkElement.target = linkTarget;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Guardar Configuración
|
||||
|
||||
### localStorage (Temporal)
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Guarda la configuración en localStorage
|
||||
*/
|
||||
function saveConfig() {
|
||||
const config = {
|
||||
enabled: document.getElementById('enabled').checked,
|
||||
showOnMobile: document.getElementById('showOnMobile').checked,
|
||||
showOnDesktop: document.getElementById('showOnDesktop').checked,
|
||||
bgColor: document.getElementById('bgColor').value,
|
||||
textColor: document.getElementById('textColor').value,
|
||||
highlightColor: document.getElementById('highlightColor').value,
|
||||
highlightText: document.getElementById('highlightText').value,
|
||||
messageText: document.getElementById('messageText').value,
|
||||
showIcon: document.getElementById('showIcon').checked,
|
||||
iconClass: document.getElementById('iconClass').value,
|
||||
showLink: document.getElementById('showLink').checked,
|
||||
linkText: document.getElementById('linkText').value,
|
||||
linkUrl: document.getElementById('linkUrl').value,
|
||||
linkTarget: document.getElementById('linkTarget').value
|
||||
};
|
||||
|
||||
localStorage.setItem('componentConfig', JSON.stringify(config));
|
||||
console.log('💾 Configuración guardada:', config);
|
||||
}
|
||||
```
|
||||
|
||||
### Archivo JSON (Persistente)
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Guarda la configuración en archivo JSON
|
||||
*/
|
||||
async function saveConfigToFile() {
|
||||
const config = {
|
||||
component: '[component-name]', // Nombre del componente en kebab-case
|
||||
version: '1.0',
|
||||
lastModified: new Date().toISOString(),
|
||||
config: {
|
||||
enabled: document.getElementById('enabled').checked,
|
||||
bgColor: document.getElementById('bgColor').value,
|
||||
textColor: document.getElementById('textColor').value,
|
||||
messageText: document.getElementById('messageText').value,
|
||||
// ... todos los campos del componente
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('./config.json', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(config, null, 2)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log('💾 Configuración guardada exitosamente');
|
||||
showNotification('Cambios guardados', 'success');
|
||||
} else {
|
||||
throw new Error('Error al guardar');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Error al guardar:', error);
|
||||
showNotification('Error al guardar cambios', 'error');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Cargar Configuración
|
||||
|
||||
### Desde localStorage
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Carga la configuración desde localStorage
|
||||
*/
|
||||
function loadConfig() {
|
||||
const saved = localStorage.getItem('componentConfig');
|
||||
|
||||
if (!saved) {
|
||||
console.log('ℹ️ No hay configuración guardada, usando valores por defecto');
|
||||
return;
|
||||
}
|
||||
|
||||
const config = JSON.parse(saved);
|
||||
|
||||
// Aplicar valores guardados
|
||||
Object.keys(config).forEach(key => {
|
||||
const element = document.getElementById(key);
|
||||
if (element) {
|
||||
if (element.type === 'checkbox') {
|
||||
element.checked = config[key];
|
||||
} else if (element.type === 'color') {
|
||||
element.value = config[key];
|
||||
// Actualizar display del hex
|
||||
const displayElement = document.getElementById(key + 'Value');
|
||||
if (displayElement) {
|
||||
displayElement.textContent = config[key].toUpperCase();
|
||||
}
|
||||
} else {
|
||||
element.value = config[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('📂 Configuración cargada:', config);
|
||||
updatePreview();
|
||||
}
|
||||
```
|
||||
|
||||
### Desde archivo JSON
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Carga la configuración desde archivo JSON
|
||||
*/
|
||||
async function loadConfigFromFile() {
|
||||
try {
|
||||
const response = await fetch('./config.json');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Config file not found');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const config = data.config;
|
||||
|
||||
// Aplicar valores cargados (igual que con localStorage)
|
||||
Object.keys(config).forEach(key => {
|
||||
const element = document.getElementById(key);
|
||||
if (element) {
|
||||
if (element.type === 'checkbox') {
|
||||
element.checked = config[key];
|
||||
} else {
|
||||
element.value = config[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('📂 Configuración cargada desde archivo:', config);
|
||||
updatePreview();
|
||||
|
||||
} catch (error) {
|
||||
console.log('ℹ️ No se encontró config.json, usando valores por defecto');
|
||||
resetToDefaults();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Reset a Valores por Defecto
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Restaura los valores por defecto
|
||||
*/
|
||||
function resetToDefaults() {
|
||||
if (!confirm('¿Estás seguro de restaurar los valores por defecto?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Valores por defecto
|
||||
const defaults = {
|
||||
enabled: true,
|
||||
showOnMobile: true,
|
||||
showOnDesktop: true,
|
||||
bgColor: '#0E2337',
|
||||
textColor: '#ffffff',
|
||||
highlightColor: '#FF8600',
|
||||
highlightText: 'Nuevo:',
|
||||
messageText: 'Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.',
|
||||
showIcon: true,
|
||||
iconClass: 'bi bi-megaphone-fill',
|
||||
showLink: true,
|
||||
linkText: 'Ver Catálogo →',
|
||||
linkUrl: '/catalogo',
|
||||
linkTarget: '_self',
|
||||
fontSize: 'normal'
|
||||
};
|
||||
|
||||
// Aplicar defaults
|
||||
Object.keys(defaults).forEach(key => {
|
||||
const element = document.getElementById(key);
|
||||
if (element) {
|
||||
if (element.type === 'checkbox') {
|
||||
element.checked = defaults[key];
|
||||
} else {
|
||||
element.value = defaults[key];
|
||||
}
|
||||
|
||||
// Actualizar display de colores
|
||||
if (element.type === 'color') {
|
||||
const displayElement = document.getElementById(key + 'Value');
|
||||
if (displayElement) {
|
||||
displayElement.textContent = defaults[key].toUpperCase();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
updatePreview();
|
||||
saveConfig();
|
||||
|
||||
console.log('🔄 Valores por defecto restaurados');
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Validación de Formularios
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Valida los campos del formulario
|
||||
*/
|
||||
function validateForm() {
|
||||
let isValid = true;
|
||||
const errors = [];
|
||||
|
||||
// 1. Validar campo requerido
|
||||
const messageText = document.getElementById('messageText').value.trim();
|
||||
if (!messageText) {
|
||||
errors.push('El mensaje principal es requerido');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// 2. Validar longitud
|
||||
if (messageText.length > 250) {
|
||||
errors.push('El mensaje no puede exceder 250 caracteres');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// 3. Validar URL
|
||||
const linkUrl = document.getElementById('linkUrl').value.trim();
|
||||
if (linkUrl && !isValidUrl(linkUrl)) {
|
||||
errors.push('La URL no es válida');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// 4. Validar formato de clase CSS
|
||||
const iconClass = document.getElementById('iconClass').value.trim();
|
||||
if (iconClass && !/^[\w\s-]+$/.test(iconClass)) {
|
||||
errors.push('La clase del icono contiene caracteres inválidos');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
alert('⚠️ Errores de validación:\n\n' + errors.join('\n'));
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida si una URL es válida
|
||||
*/
|
||||
function isValidUrl(string) {
|
||||
// Permitir rutas relativas
|
||||
if (string.startsWith('/')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Validar URLs absolutas
|
||||
try {
|
||||
new URL(string);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Sistema de Notificaciones
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Muestra notificación temporal
|
||||
*/
|
||||
function showNotification(message, type = 'success') {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `alert alert-${type === 'success' ? 'success' : 'danger'} position-fixed top-0 start-50 translate-middle-x mt-3`;
|
||||
notification.style.zIndex = '9999';
|
||||
notification.innerHTML = `
|
||||
<i class="bi bi-${type === 'success' ? 'check-circle' : 'exclamation-circle'} me-2"></i>
|
||||
${message}
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Uso
|
||||
showNotification('Cambios guardados', 'success');
|
||||
showNotification('Error al guardar cambios', 'error');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Auto-save (Opcional)
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Auto-guarda cada vez que cambia un campo
|
||||
*/
|
||||
function initializeAutoSave() {
|
||||
const fields = document.querySelectorAll('input, select, textarea');
|
||||
|
||||
fields.forEach(field => {
|
||||
field.addEventListener('change', function() {
|
||||
saveConfig();
|
||||
console.log('💾 Auto-guardado');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Llamar después de initializeEventListeners()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Volver al Índice
|
||||
|
||||
[← Volver al README](README.md)
|
||||
443
_planificacion/01-design-system/12-PERSISTENCIA-JSON.md
Normal file
443
_planificacion/01-design-system/12-PERSISTENCIA-JSON.md
Normal file
@@ -0,0 +1,443 @@
|
||||
# 💾 PERSISTENCIA EN ARCHIVOS JSON
|
||||
|
||||
## Estructura del Archivo config.json
|
||||
|
||||
Cada componente DEBE tener su archivo `config.json` en su carpeta:
|
||||
|
||||
```json
|
||||
{
|
||||
"component": "[component-name]",
|
||||
"version": "1.0",
|
||||
"lastModified": "2025-01-15T10:30:00Z",
|
||||
"config": {
|
||||
"enabled": true,
|
||||
"showOnMobile": true,
|
||||
"showOnDesktop": true,
|
||||
"bgColor": "#0E2337",
|
||||
"textColor": "#ffffff",
|
||||
"highlightColor": "#FF8600",
|
||||
"linkHoverColor": "#FF6B35",
|
||||
"fontSize": "normal",
|
||||
"showIcon": true,
|
||||
"iconClass": "bi bi-icon-name",
|
||||
"highlightText": "Texto destacado",
|
||||
"messageText": "Mensaje principal del componente",
|
||||
"showLink": true,
|
||||
"linkText": "Texto del enlace",
|
||||
"linkUrl": "/ruta",
|
||||
"linkTarget": "_self"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Campos del Metadata
|
||||
|
||||
| Campo | Tipo | Descripción |
|
||||
|-------|------|-------------|
|
||||
| `component` | string | Nombre del componente (kebab-case) |
|
||||
| `version` | string | Versión del config (semver) |
|
||||
| `lastModified` | string | Timestamp ISO 8601 de última modificación |
|
||||
| `config` | object | Objeto con la configuración del componente |
|
||||
|
||||
---
|
||||
|
||||
## Funciones de Persistencia
|
||||
|
||||
### Guardar Configuración
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Guarda la configuración en archivo JSON
|
||||
*/
|
||||
async function saveConfig() {
|
||||
const config = {
|
||||
component: '[component-name]', // Nombre del componente en kebab-case
|
||||
version: '1.0',
|
||||
lastModified: new Date().toISOString(),
|
||||
config: {
|
||||
enabled: document.getElementById('enabled').checked,
|
||||
showOnMobile: document.getElementById('showOnMobile').checked,
|
||||
showOnDesktop: document.getElementById('showOnDesktop').checked,
|
||||
bgColor: document.getElementById('bgColor').value,
|
||||
textColor: document.getElementById('textColor').value,
|
||||
highlightColor: document.getElementById('highlightColor').value,
|
||||
linkHoverColor: document.getElementById('linkHoverColor').value,
|
||||
fontSize: document.getElementById('fontSize').value,
|
||||
showIcon: document.getElementById('showIcon').checked,
|
||||
iconClass: document.getElementById('iconClass').value,
|
||||
highlightText: document.getElementById('highlightText').value,
|
||||
messageText: document.getElementById('messageText').value,
|
||||
showLink: document.getElementById('showLink').checked,
|
||||
linkText: document.getElementById('linkText').value,
|
||||
linkUrl: document.getElementById('linkUrl').value,
|
||||
linkTarget: document.getElementById('linkTarget').value
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
// Guardar en archivo JSON
|
||||
const response = await fetch('./config.json', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(config, null, 2)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log('💾 Configuración guardada exitosamente');
|
||||
showNotification('Cambios guardados', 'success');
|
||||
} else {
|
||||
throw new Error('Error al guardar');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Error al guardar:', error);
|
||||
showNotification('Error al guardar cambios', 'error');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Cargar Configuración
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Carga la configuración desde archivo JSON
|
||||
*/
|
||||
async function loadConfig() {
|
||||
try {
|
||||
const response = await fetch('./config.json');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Config file not found');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const config = data.config;
|
||||
|
||||
// Validar estructura
|
||||
if (!validateConfigStructure(data)) {
|
||||
throw new Error('Invalid config structure');
|
||||
}
|
||||
|
||||
// Aplicar valores cargados
|
||||
if (config.enabled !== undefined) {
|
||||
document.getElementById('enabled').checked = config.enabled;
|
||||
}
|
||||
if (config.showOnMobile !== undefined) {
|
||||
document.getElementById('showOnMobile').checked = config.showOnMobile;
|
||||
}
|
||||
if (config.showOnDesktop !== undefined) {
|
||||
document.getElementById('showOnDesktop').checked = config.showOnDesktop;
|
||||
}
|
||||
if (config.bgColor) {
|
||||
document.getElementById('bgColor').value = config.bgColor;
|
||||
document.getElementById('bgColorValue').textContent = config.bgColor.toUpperCase();
|
||||
}
|
||||
if (config.textColor) {
|
||||
document.getElementById('textColor').value = config.textColor;
|
||||
document.getElementById('textColorValue').textContent = config.textColor.toUpperCase();
|
||||
}
|
||||
if (config.highlightColor) {
|
||||
document.getElementById('highlightColor').value = config.highlightColor;
|
||||
document.getElementById('highlightColorValue').textContent = config.highlightColor.toUpperCase();
|
||||
}
|
||||
if (config.fontSize) {
|
||||
document.getElementById('fontSize').value = config.fontSize;
|
||||
}
|
||||
if (config.showIcon !== undefined) {
|
||||
document.getElementById('showIcon').checked = config.showIcon;
|
||||
}
|
||||
if (config.iconClass) {
|
||||
document.getElementById('iconClass').value = config.iconClass;
|
||||
}
|
||||
if (config.highlightText) {
|
||||
document.getElementById('highlightText').value = config.highlightText;
|
||||
}
|
||||
if (config.messageText) {
|
||||
document.getElementById('messageText').value = config.messageText;
|
||||
// Actualizar contador si existe
|
||||
const counter = document.getElementById('messageTextCount');
|
||||
if (counter) {
|
||||
counter.textContent = config.messageText.length;
|
||||
}
|
||||
}
|
||||
if (config.showLink !== undefined) {
|
||||
document.getElementById('showLink').checked = config.showLink;
|
||||
}
|
||||
if (config.linkText) {
|
||||
document.getElementById('linkText').value = config.linkText;
|
||||
}
|
||||
if (config.linkUrl) {
|
||||
document.getElementById('linkUrl').value = config.linkUrl;
|
||||
}
|
||||
if (config.linkTarget) {
|
||||
document.getElementById('linkTarget').value = config.linkTarget;
|
||||
}
|
||||
|
||||
console.log('📂 Configuración cargada:', config);
|
||||
updatePreview();
|
||||
|
||||
} catch (error) {
|
||||
console.log('ℹ️ No se encontró config.json, usando valores por defecto');
|
||||
resetToDefaults();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Validación de JSON
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Valida la estructura del archivo config.json
|
||||
*/
|
||||
function validateConfigStructure(data) {
|
||||
const required = ['component', 'version', 'config'];
|
||||
|
||||
// Verificar campos requeridos
|
||||
for (const field of required) {
|
||||
if (!data.hasOwnProperty(field)) {
|
||||
console.error(`❌ Campo requerido faltante: ${field}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Verificar que config sea un objeto
|
||||
if (typeof data.config !== 'object' || data.config === null) {
|
||||
console.error('❌ El campo "config" debe ser un objeto');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sistema de Notificaciones
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Muestra notificación temporal
|
||||
*/
|
||||
function showNotification(message, type = 'success') {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `alert alert-${type === 'success' ? 'success' : 'danger'} position-fixed top-0 start-50 translate-middle-x mt-3`;
|
||||
notification.style.zIndex = '9999';
|
||||
notification.innerHTML = `
|
||||
<i class="bi bi-${type === 'success' ? 'check-circle' : 'exclamation-circle'} me-2"></i>
|
||||
${message}
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Uso
|
||||
showNotification('Cambios guardados', 'success');
|
||||
showNotification('Error al guardar cambios', 'error');
|
||||
showNotification('Configuración restaurada', 'success');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migración de localStorage a JSON
|
||||
|
||||
Si tienes datos en localStorage que quieres migrar a JSON:
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Migra la configuración de localStorage a archivo JSON
|
||||
*/
|
||||
async function migrateFromLocalStorage() {
|
||||
const saved = localStorage.getItem('topBarConfig');
|
||||
|
||||
if (!saved) {
|
||||
console.log('ℹ️ No hay datos en localStorage para migrar');
|
||||
return;
|
||||
}
|
||||
|
||||
const config = JSON.parse(saved);
|
||||
|
||||
// Crear estructura de config.json
|
||||
const jsonConfig = {
|
||||
component: '[component-name]', // Nombre del componente en kebab-case
|
||||
version: '1.0',
|
||||
lastModified: new Date().toISOString(),
|
||||
config: config
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('./config.json', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(jsonConfig, null, 2)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log('✅ Migración exitosa de localStorage a JSON');
|
||||
// Opcional: limpiar localStorage
|
||||
localStorage.removeItem('topBarConfig');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Error al migrar:', error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Manejo de Errores
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Guarda con manejo completo de errores
|
||||
*/
|
||||
async function saveConfigWithErrorHandling() {
|
||||
// 1. Validar antes de guardar
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Preparar datos
|
||||
const config = buildConfigObject();
|
||||
|
||||
// 3. Intentar guardar
|
||||
try {
|
||||
const response = await fetch('./config.json', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(config, null, 2)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
console.log('💾 Configuración guardada exitosamente');
|
||||
showNotification('Cambios guardados correctamente', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error al guardar:', error);
|
||||
|
||||
// Notificar al usuario
|
||||
showNotification('Error al guardar. Verifica la conexión.', 'error');
|
||||
|
||||
// Fallback: guardar en localStorage temporalmente
|
||||
localStorage.setItem('topBarConfig_backup', JSON.stringify(config));
|
||||
console.log('💾 Backup guardado en localStorage');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construye el objeto de configuración
|
||||
*/
|
||||
function buildConfigObject() {
|
||||
return {
|
||||
component: '[component-name]', // Nombre del componente en kebab-case
|
||||
version: '1.0',
|
||||
lastModified: new Date().toISOString(),
|
||||
config: {
|
||||
enabled: document.getElementById('enabled').checked,
|
||||
bgColor: document.getElementById('bgColor').value,
|
||||
// ... resto de campos
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Backup y Restore
|
||||
|
||||
### Crear Backup
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Crea un backup de la configuración actual
|
||||
*/
|
||||
async function createBackup() {
|
||||
try {
|
||||
const response = await fetch('./config.json');
|
||||
const config = await response.json();
|
||||
|
||||
// Agregar timestamp al nombre del backup
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const backupName = `config_backup_${timestamp}.json`;
|
||||
|
||||
// Descargar como archivo
|
||||
const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = backupName;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
showNotification('Backup creado exitosamente', 'success');
|
||||
} catch (error) {
|
||||
console.error('❌ Error al crear backup:', error);
|
||||
showNotification('Error al crear backup', 'error');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Restaurar Backup
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Restaura un backup
|
||||
*/
|
||||
function restoreBackup(file) {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = async function(e) {
|
||||
try {
|
||||
const config = JSON.parse(e.target.result);
|
||||
|
||||
// Validar estructura
|
||||
if (!validateConfigStructure(config)) {
|
||||
throw new Error('Invalid backup structure');
|
||||
}
|
||||
|
||||
// Aplicar configuración
|
||||
const data = config.config;
|
||||
Object.keys(data).forEach(key => {
|
||||
const element = document.getElementById(key);
|
||||
if (element) {
|
||||
if (element.type === 'checkbox') {
|
||||
element.checked = data[key];
|
||||
} else {
|
||||
element.value = data[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
updatePreview();
|
||||
showNotification('Backup restaurado exitosamente', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error al restaurar backup:', error);
|
||||
showNotification('Error al restaurar backup', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Volver al Índice
|
||||
|
||||
[← Volver al README](README.md)
|
||||
473
_planificacion/01-design-system/13-PANEL-ADMINISTRACION.md
Normal file
473
_planificacion/01-design-system/13-PANEL-ADMINISTRACION.md
Normal file
@@ -0,0 +1,473 @@
|
||||
# 🎛️ PANEL DE ADMINISTRACIÓN PRINCIPAL
|
||||
|
||||
## Arquitectura del Sistema
|
||||
|
||||
El panel de administración utiliza un **sistema de tabs (pestañas)** de Bootstrap 5, donde todos los componentes existen en una sola página y se alternan mediante JavaScript.
|
||||
|
||||
### Características Principales
|
||||
|
||||
- ✅ **Single Page**: Todos los componentes en un solo archivo `main.php`
|
||||
- ✅ **Bootstrap Tabs**: Navegación mediante `nav-tabs` y `tab-pane`
|
||||
- ✅ **Carga Modular**: Cada componente se incluye con `require_once`
|
||||
- ✅ **Botones Globales**: Save/Cancel compartidos para todos los componentes
|
||||
- ✅ **Sin Recargas**: Cambio de tabs sin reload de página
|
||||
|
||||
---
|
||||
|
||||
## Estructura del main.php
|
||||
|
||||
```php
|
||||
<?php
|
||||
/**
|
||||
* Admin Panel - Main Page
|
||||
*/
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="wrap apus-admin">
|
||||
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
|
||||
<p class="description">Configure los componentes del tema</p>
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<!-- Tabs aquí -->
|
||||
</ul>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<div class="tab-content mt-3">
|
||||
<?php
|
||||
// Componentes incluidos aquí
|
||||
?>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons (Global) -->
|
||||
<div class="d-flex justify-content-end gap-2 p-3 rounded border mt-4"
|
||||
style="background-color: #f8f9fa;">
|
||||
<button type="button" class="btn btn-outline-secondary" id="cancelChanges">
|
||||
<i class="bi bi-x-circle me-1"></i>
|
||||
Cancelar
|
||||
</button>
|
||||
<button type="button" id="saveSettings"
|
||||
class="btn fw-semibold text-white"
|
||||
style="background-color: #FF8600; border-color: #FF8600;">
|
||||
<i class="bi bi-check-circle me-1"></i>
|
||||
Guardar Cambios
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Agregar un Nuevo Tab
|
||||
|
||||
### Paso 1: Agregar el Tab en la Navegación
|
||||
|
||||
```php
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#[tabId]"
|
||||
href="#[tabId]"
|
||||
role="tab">
|
||||
<i class="bi bi-[icon-class] me-2"></i>
|
||||
[Nombre del Componente]
|
||||
</a>
|
||||
</li>
|
||||
<!-- Más tabs... -->
|
||||
</ul>
|
||||
```
|
||||
|
||||
**Reemplazar:**
|
||||
- `[tabId]`: ID único del tab (ej: `notificationBarTab`, `siteFooterTab`)
|
||||
- `[icon-class]`: Clase del icono de Bootstrap Icons (ej: `megaphone-fill`)
|
||||
- `[Nombre del Componente]`: Nombre visible del tab (ej: "Barra de Notificaciones")
|
||||
|
||||
### Paso 2: Incluir el Componente en Tab Content
|
||||
|
||||
```php
|
||||
<div class="tab-content mt-3">
|
||||
<?php
|
||||
/**
|
||||
* [Component Name] Component
|
||||
* Archivo: Admin/Components/component-[name].php
|
||||
*/
|
||||
require_once APUS_ADMIN_PANEL_PATH . 'Admin/Components/component-[name].php';
|
||||
?>
|
||||
|
||||
<!-- Más componentes... -->
|
||||
</div>
|
||||
```
|
||||
|
||||
**Reemplazar:**
|
||||
- `[Component Name]`: Nombre descriptivo del componente
|
||||
- `[name]`: Nombre del archivo (ej: `notification-bar`, `site-footer`)
|
||||
|
||||
### Paso 3: Crear el Archivo del Componente
|
||||
|
||||
Crear `Admin/Components/component-[name].php`:
|
||||
|
||||
```php
|
||||
<!-- Tab Pane: [Component Name] -->
|
||||
<div class="tab-pane fade show active" id="[tabId]" role="tabpanel">
|
||||
|
||||
<!-- Header del Tab -->
|
||||
<div class="rounded p-4 mb-4 shadow text-white"
|
||||
style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%);
|
||||
border-left: 4px solid #FF8600;">
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
|
||||
<div>
|
||||
<h3 class="h4 mb-1 fw-bold">
|
||||
<i class="bi bi-[icon-class] me-2" style="color: #FF8600;"></i>
|
||||
[Título del Componente]
|
||||
</h3>
|
||||
<p class="mb-0 small" style="opacity: 0.85;">
|
||||
[Descripción breve del componente]
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-light" id="reset[Component]Defaults">
|
||||
<i class="bi bi-arrow-counterclockwise me-1"></i>
|
||||
Restaurar valores por defecto
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grid de Configuración -->
|
||||
<div class="row g-3">
|
||||
<!-- Columna izquierda: Configuración -->
|
||||
<div class="col-lg-6">
|
||||
<!-- Cards de configuración -->
|
||||
</div>
|
||||
|
||||
<!-- Columna derecha: Preview -->
|
||||
<div class="col-lg-6">
|
||||
<!-- Card de vista previa -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /tab-pane -->
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Iconos Sugeridos por Tipo de Componente
|
||||
|
||||
| Tipo de Componente | Icono | Clase Bootstrap Icons |
|
||||
|--------------------|-------|----------------------|
|
||||
| Notificaciones/Anuncios | 📢 | `bi-megaphone-fill` |
|
||||
| Navegación/Menú | 📋 | `bi-layout-text-window` |
|
||||
| Sección Hero | 🖼️ | `bi-image` |
|
||||
| Footer | ⬇️ | `bi-box-arrow-down` |
|
||||
| Formularios | 📝 | `bi-clipboard-check` |
|
||||
| Call-to-Action | 👆 | `bi-cursor-fill` |
|
||||
| Testimonios | 💬 | `bi-chat-quote` |
|
||||
| Configuración | ⚙️ | `bi-gear-fill` |
|
||||
|
||||
---
|
||||
|
||||
## Ejemplo Completo: Agregar Componente de Navegación
|
||||
|
||||
### 1. Agregar Tab en main.php
|
||||
|
||||
```php
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<!-- Tab existente -->
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#topBarTab"
|
||||
href="#topBarTab"
|
||||
role="tab">
|
||||
<i class="bi bi-megaphone-fill me-2"></i>
|
||||
Top Bar
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- NUEVO TAB -->
|
||||
<li class="nav-item">
|
||||
<a class="nav-link"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#navbarTab"
|
||||
href="#navbarTab"
|
||||
role="tab">
|
||||
<i class="bi bi-layout-text-window me-2"></i>
|
||||
Navbar
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
**Notas:**
|
||||
- El primer tab tiene `class="nav-link active"`
|
||||
- Los demás tabs solo tienen `class="nav-link"`
|
||||
|
||||
### 2. Incluir Componente en Tab Content
|
||||
|
||||
```php
|
||||
<div class="tab-content mt-3">
|
||||
<?php
|
||||
// Componente existente
|
||||
require_once APUS_ADMIN_PANEL_PATH . 'Admin/Components/component-top-bar.php';
|
||||
|
||||
// NUEVO COMPONENTE
|
||||
require_once APUS_ADMIN_PANEL_PATH . 'Admin/Components/component-navbar.php';
|
||||
?>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 3. Crear component-navbar.php
|
||||
|
||||
Crear archivo: `Admin/Components/component-navbar.php`
|
||||
|
||||
```php
|
||||
<!-- Tab Pane: Navbar -->
|
||||
<div class="tab-pane fade" id="navbarTab" role="tabpanel">
|
||||
|
||||
<!-- Header del Tab -->
|
||||
<div class="rounded p-4 mb-4 shadow text-white"
|
||||
style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%);
|
||||
border-left: 4px solid #FF8600;">
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
|
||||
<div>
|
||||
<h3 class="h4 mb-1 fw-bold">
|
||||
<i class="bi bi-layout-text-window me-2" style="color: #FF8600;"></i>
|
||||
Configuración del Navbar
|
||||
</h3>
|
||||
<p class="mb-0 small" style="opacity: 0.85;">
|
||||
Gestiona los colores, estilo y comportamiento del menú de navegación
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-light" id="resetNavbarDefaults">
|
||||
<i class="bi bi-arrow-counterclockwise me-1"></i>
|
||||
Restaurar valores por defecto
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grid de Configuración -->
|
||||
<div class="row g-3">
|
||||
<!-- Configuración -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-palette me-2" style="color: #FF8600;"></i>
|
||||
Colores
|
||||
</h5>
|
||||
<!-- Campos de configuración aquí -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Vista Previa -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #FF8600;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-eye me-2" style="color: #FF8600;"></i>
|
||||
Vista Previa
|
||||
</h5>
|
||||
<!-- Preview del componente aquí -->
|
||||
<div id="navbarPreview">
|
||||
<!-- HTML idéntico al front-end -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /tab-pane -->
|
||||
```
|
||||
|
||||
**Notas:**
|
||||
- El primer tab-pane tiene `class="tab-pane fade show active"`
|
||||
- Los demás tab-panes tienen `class="tab-pane fade"`
|
||||
|
||||
---
|
||||
|
||||
## Orden de los Tabs
|
||||
|
||||
### Por Prioridad
|
||||
|
||||
Organizar los tabs de más importante a menos importante:
|
||||
|
||||
1. Componentes críticos/visibles (Header, Navbar, Hero)
|
||||
2. Componentes de contenido (Secciones, CTAs)
|
||||
3. Componentes complementarios (Footer, Forms)
|
||||
4. Configuraciones generales
|
||||
|
||||
### Ejemplo de Orden
|
||||
|
||||
```php
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li class="nav-item"><!-- Barra de Notificaciones --></li>
|
||||
<li class="nav-item"><!-- Navbar --></li>
|
||||
<li class="nav-item"><!-- Hero Section --></li>
|
||||
<li class="nav-item"><!-- Call-to-Action --></li>
|
||||
<li class="nav-item"><!-- Footer --></li>
|
||||
<li class="nav-item"><!-- Configuración General --></li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Botones Globales Save/Cancel
|
||||
|
||||
Los botones de guardar y cancelar son **globales** para todos los tabs:
|
||||
|
||||
```php
|
||||
<div class="d-flex justify-content-end gap-2 p-3 rounded border mt-4"
|
||||
style="background-color: #f8f9fa; border-color: #e9ecef !important;">
|
||||
|
||||
<!-- Cancelar -->
|
||||
<button type="button" class="btn btn-outline-secondary" id="cancelChanges">
|
||||
<i class="bi bi-x-circle me-1"></i>
|
||||
Cancelar
|
||||
</button>
|
||||
|
||||
<!-- Guardar -->
|
||||
<button type="button" id="saveSettings"
|
||||
class="btn fw-semibold text-white"
|
||||
style="background-color: #FF8600; border-color: #FF8600;"
|
||||
disabled>
|
||||
<i class="bi bi-check-circle me-1"></i>
|
||||
Guardar Cambios
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
### JavaScript para Activar/Desactivar Guardar
|
||||
|
||||
```javascript
|
||||
// Detectar cambios en cualquier campo
|
||||
document.querySelectorAll('input, select, textarea').forEach(field => {
|
||||
field.addEventListener('change', function() {
|
||||
// Activar botón de guardar
|
||||
document.getElementById('saveSettings').disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
// Guardar cambios
|
||||
document.getElementById('saveSettings').addEventListener('click', function() {
|
||||
// Guardar configuración de todos los componentes
|
||||
saveAllConfigs();
|
||||
|
||||
// Desactivar botón de guardar
|
||||
this.disabled = true;
|
||||
});
|
||||
|
||||
// Cancelar cambios
|
||||
document.getElementById('cancelChanges').addEventListener('click', function() {
|
||||
if (confirm('¿Descartar todos los cambios sin guardar?')) {
|
||||
// Recargar configuraciones originales
|
||||
loadAllConfigs();
|
||||
|
||||
// Desactivar botón de guardar
|
||||
document.getElementById('saveSettings').disabled = true;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Responsive Behavior
|
||||
|
||||
### Desktop (≥992px)
|
||||
- Tabs en una sola línea horizontal
|
||||
- Grid de 2 columnas (configuración + preview)
|
||||
- Espaciado completo
|
||||
|
||||
### Tablet (768px - 991px)
|
||||
- Tabs pueden hacer wrap a 2 líneas
|
||||
- Grid de 2 columnas (se mantiene)
|
||||
- Espaciado reducido
|
||||
|
||||
### Mobile (<768px)
|
||||
- Tabs en scroll horizontal o stacked vertical
|
||||
- Grid stacked (1 columna)
|
||||
- Botones Save/Cancel pueden hacer stack
|
||||
|
||||
```css
|
||||
@media (max-width: 991px) {
|
||||
.nav-tabs {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link {
|
||||
font-size: 0.9rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.nav-tabs {
|
||||
overflow-x: auto;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-item {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Funcionalidad Adicional (Opcional)
|
||||
|
||||
### Badges de Estado en Tabs
|
||||
|
||||
```php
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" data-bs-target="#[tabId]" role="tab">
|
||||
<i class="bi bi-[icon-class] me-2"></i>
|
||||
[Nombre del Componente]
|
||||
<span class="badge bg-success ms-2" style="font-size: 0.65rem;">Activo</span>
|
||||
</a>
|
||||
</li>
|
||||
```
|
||||
|
||||
**Estados posibles:**
|
||||
- `bg-success`: Activo
|
||||
- `bg-secondary`: Inactivo
|
||||
- `bg-warning text-dark`: Requiere atención
|
||||
|
||||
### Indicador de Cambios Sin Guardar
|
||||
|
||||
```javascript
|
||||
// Agregar asterisco al tab si tiene cambios sin guardar
|
||||
function markTabAsModified(tabId) {
|
||||
const tabLink = document.querySelector(`a[data-bs-target="#${tabId}"]`);
|
||||
if (!tabLink.querySelector('.modified-indicator')) {
|
||||
tabLink.innerHTML += ' <span class="modified-indicator" style="color: #FF8600;">*</span>';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checklist de Implementación
|
||||
|
||||
Cuando agregues un nuevo componente al panel, asegúrate de:
|
||||
|
||||
- [ ] Agregar el tab en la navegación (`<ul class="nav nav-tabs">`)
|
||||
- [ ] Crear el archivo PHP del componente (`component-[name].php`)
|
||||
- [ ] Incluir el componente en tab-content con `require_once`
|
||||
- [ ] Crear archivo CSS del componente (`component-[name].css`)
|
||||
- [ ] Crear archivo JS del componente (`component-[name].js`)
|
||||
- [ ] Crear archivo de configuración JSON (`[name]-config.json`)
|
||||
- [ ] Agregar estilos específicos del componente
|
||||
- [ ] Implementar vista previa en tiempo real
|
||||
- [ ] Conectar con sistema de guardar/cancelar global
|
||||
- [ ] Probar responsive behavior
|
||||
- [ ] Verificar que el tab-pane tenga el ID correcto
|
||||
- [ ] Verificar que el primer tab y tab-pane tengan `active` y `show`
|
||||
|
||||
---
|
||||
|
||||
## Volver al Índice
|
||||
|
||||
[← Volver al README](README.md)
|
||||
395
_planificacion/01-design-system/14-CONFLICTOS-WORDPRESS.md
Normal file
395
_planificacion/01-design-system/14-CONFLICTOS-WORDPRESS.md
Normal file
@@ -0,0 +1,395 @@
|
||||
# ⚠️ CONFLICTOS CON WORDPRESS
|
||||
|
||||
## Conflicto 1: Bootstrap `.card` vs WordPress `.card`
|
||||
|
||||
### El Problema
|
||||
|
||||
WordPress Core tiene esta regla CSS que limita el ancho de las cards:
|
||||
|
||||
```css
|
||||
/* WordPress Core CSS */
|
||||
.card {
|
||||
max-width: 520px;
|
||||
}
|
||||
```
|
||||
|
||||
Bootstrap 5 usa `.card` como componente principal:
|
||||
|
||||
```html
|
||||
<div class="card">
|
||||
<div class="card-body">...</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**CONFLICTO**: Cuando usas `<div class="card">` de Bootstrap dentro del admin de WordPress, WordPress aplica automáticamente `max-width: 520px`, limitando el ancho de tus cards.
|
||||
|
||||
---
|
||||
|
||||
### ❌ SOLUCIÓN INCORRECTA
|
||||
|
||||
**NO cambies `.card` por `.apus-card` o cualquier otra clase custom.**
|
||||
|
||||
**Razones:**
|
||||
|
||||
1. Bootstrap `.card` es un **sistema completo** con múltiples clases relacionadas:
|
||||
- `.card-body`
|
||||
- `.card-header`
|
||||
- `.card-footer`
|
||||
- `.card-title`
|
||||
- `.card-text`
|
||||
- `.card-img-top`
|
||||
- Y muchas más...
|
||||
|
||||
2. Si cambias `.card` a `.apus-card`, pierdes **TODOS** los estilos de Bootstrap
|
||||
|
||||
3. Tendrías que recrear manualmente TODO el sistema de cards
|
||||
|
||||
---
|
||||
|
||||
### ✅ SOLUCIÓN CORRECTA
|
||||
|
||||
**Opción 1: Wrapper con Mayor Especificidad (RECOMENDADA)**
|
||||
|
||||
Agregar una clase wrapper en el `<body>` y sobreescribir el CSS de WordPress:
|
||||
|
||||
```html
|
||||
<body class="apus-admin" style="font-family: 'Poppins', sans-serif; background-color: #f8f9fa;">
|
||||
<div class="container-fluid py-4" style="max-width: 1400px;">
|
||||
<!-- Todos tus cards de Bootstrap funcionan normalmente -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<!-- Contenido -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
```
|
||||
|
||||
**CSS:**
|
||||
```css
|
||||
/* Sobreescribir el max-width de WordPress */
|
||||
.apus-admin .card {
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
/* Asegurar que funcione en todos los contextos */
|
||||
body.apus-admin .card,
|
||||
#apusAdminPanel .card {
|
||||
max-width: none !important;
|
||||
}
|
||||
```
|
||||
|
||||
**Opción 2: Inline Style (Rápido)**
|
||||
|
||||
Si solo tienes unos pocos cards:
|
||||
|
||||
```html
|
||||
<div class="card shadow-sm" style="max-width: none;">
|
||||
<div class="card-body">
|
||||
<!-- Contenido -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Opción 3: CSS Global**
|
||||
|
||||
```html
|
||||
<style>
|
||||
/* Sobreescribir WordPress max-width en todos los cards del admin */
|
||||
body .card {
|
||||
max-width: none !important;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Conflicto 2: Form Switches (CRÍTICO)
|
||||
|
||||
### El Problema
|
||||
|
||||
Los switches de Bootstrap muestran **dos círculos** en lugar de un círculo que se desliza.
|
||||
|
||||
### Causa Raíz
|
||||
|
||||
1. **Pseudo-elemento `::before` de WordPress**: Muestra un SVG de checkmark (✓)
|
||||
2. **`background-image` de Bootstrap**: Muestra el círculo del switch
|
||||
3. **`background-size` incorrecto**: WordPress usa `auto` en lugar de `contain`
|
||||
|
||||
**Resultado**: Dos elementos visuales superpuestos en el switch.
|
||||
|
||||
---
|
||||
|
||||
### ✅ Solución Definitiva
|
||||
|
||||
```css
|
||||
/* Eliminar completamente pseudo-elementos de WordPress */
|
||||
body .form-switch .form-check-input[type="checkbox"]::before,
|
||||
body .form-switch .form-check-input[type="checkbox"]::after,
|
||||
.apus-admin .form-switch .form-check-input[type="checkbox"]::before,
|
||||
.apus-admin .form-switch .form-check-input[type="checkbox"]::after {
|
||||
content: none !important;
|
||||
display: none !important;
|
||||
background-image: none !important;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
}
|
||||
|
||||
/* Configurar correctamente el background-size y repeat */
|
||||
body .form-switch .form-check-input[type="checkbox"],
|
||||
.apus-admin .form-switch .form-check-input[type="checkbox"] {
|
||||
background-size: contain !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-position: left center !important;
|
||||
}
|
||||
|
||||
body .form-switch .form-check-input[type="checkbox"]:checked,
|
||||
.apus-admin .form-switch .form-check-input[type="checkbox"]:checked {
|
||||
background-size: contain !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-position: right center !important;
|
||||
}
|
||||
```
|
||||
|
||||
**Qué hace cada parte:**
|
||||
|
||||
1. **Elimina `::before` y `::after`**: Anula completamente los pseudo-elementos de WordPress
|
||||
2. **`content: none !important`**: Elimina cualquier contenido SVG de WordPress
|
||||
3. **`display: none !important`**: Oculta el pseudo-elemento aunque tenga contenido
|
||||
4. **`background-size: contain`**: Asegura que el círculo de Bootstrap se escale correctamente
|
||||
5. **`background-repeat: no-repeat`**: Previene que el círculo se duplique
|
||||
6. **`background-position`**: Controla la posición del círculo (left cuando OFF, right cuando ON)
|
||||
|
||||
---
|
||||
|
||||
## Conflicto 3: Alineación Vertical de Switches (CRÍTICO)
|
||||
|
||||
### El Problema
|
||||
|
||||
Los labels de los switches aparecen **desalineados verticalmente** con los switches, con una diferencia de aproximadamente **10.5px**.
|
||||
|
||||
### Causa Raíz
|
||||
|
||||
1. WordPress aplica `label { vertical-align: middle; }` globalmente
|
||||
2. Bootstrap usa `display: block` + `float: left` en `.form-check`
|
||||
3. Conflicto de layout entre elementos flotantes y `vertical-align`
|
||||
|
||||
---
|
||||
|
||||
### ✅ Solución Definitiva
|
||||
|
||||
```css
|
||||
/* Fix alineación vertical de labels en switches */
|
||||
#topBarTab .form-check.form-switch {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
#topBarTab .form-switch .form-check-input {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
#topBarTab .form-switch .form-check-label,
|
||||
.form-switch .form-check-label.small {
|
||||
line-height: 16px !important;
|
||||
padding-top: 0 !important;
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
```
|
||||
|
||||
**Qué hace cada parte:**
|
||||
|
||||
1. **`display: flex`**: Convierte el contenedor en flexbox
|
||||
2. **`align-items: center`**: Alinea todos los hijos verticalmente por su centro
|
||||
3. **`line-height: 16px`**: Coincide con la altura del switch para alineación perfecta
|
||||
4. **Márgenes en 0**: Elimina espacios verticales no deseados
|
||||
|
||||
**Resultado**: Diferencia de **0px** entre el centro del switch y el centro del label.
|
||||
|
||||
---
|
||||
|
||||
## Conflicto 4: `.button` (Raro)
|
||||
|
||||
WordPress también estiliza `.button`. **Solución**: Siempre usa `.btn` de Bootstrap:
|
||||
|
||||
```html
|
||||
<!-- ✅ Seguro -->
|
||||
<button class="btn btn-primary">OK</button>
|
||||
|
||||
<!-- ❌ Conflicto con WP -->
|
||||
<button class="button">OK</button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Conflicto 5: `.row` (Raro)
|
||||
|
||||
Algunos temas de WordPress pueden estilizar `.row`. Si tienes problemas:
|
||||
|
||||
```css
|
||||
.apus-admin .row {
|
||||
/* Reset de cualquier estilo de WordPress */
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-right: calc(var(--bs-gutter-x) * -0.5);
|
||||
margin-left: calc(var(--bs-gutter-x) * -0.5);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Conflicto 6: Colores de Links
|
||||
|
||||
WordPress usa colores específicos. Asegúrate de que tu CSS tenga mayor especificidad:
|
||||
|
||||
```css
|
||||
/* WordPress puede tener reglas globales */
|
||||
a { color: #0073aa; }
|
||||
|
||||
/* Tu CSS debe ser más específico */
|
||||
.apus-admin a { color: #FF8600; }
|
||||
.apus-admin .card a { color: #FF8600; }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template Actualizado con Todos los Fixes
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin: [Component Name]</title>
|
||||
|
||||
<!-- Bootstrap 5.3.2 -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/Css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/Font/bootstrap-icons.min.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="../../Css/style.css">
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* FIX 1: Sobreescribir max-width de WordPress en Bootstrap cards */
|
||||
body .card,
|
||||
.apus-admin .card {
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
/* FIX 2: Eliminar pseudo-elementos de WordPress en switches */
|
||||
body .form-switch .form-check-input[type="checkbox"]::before,
|
||||
body .form-switch .form-check-input[type="checkbox"]::after {
|
||||
content: none !important;
|
||||
display: none !important;
|
||||
background-image: none !important;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
}
|
||||
|
||||
/* FIX 3: Configurar background correctamente en switches */
|
||||
body .form-switch .form-check-input[type="checkbox"] {
|
||||
background-size: contain !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-position: left center !important;
|
||||
}
|
||||
|
||||
body .form-switch .form-check-input[type="checkbox"]:checked {
|
||||
background-position: right center !important;
|
||||
}
|
||||
|
||||
/* FIX 4: Alineación vertical de labels en switches */
|
||||
.form-check.form-switch {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.form-switch .form-check-input {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.form-switch .form-check-label {
|
||||
line-height: 16px !important;
|
||||
padding-top: 0 !important;
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 991px) {
|
||||
.tab-header { padding: 0.75rem; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="apus-admin">
|
||||
<!-- Contenido del admin panel -->
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checklist Anti-Conflictos WordPress
|
||||
|
||||
Antes de implementar un componente, verificar:
|
||||
|
||||
- [ ] Agregar clase `.apus-admin` al `<body>`
|
||||
- [ ] **CRÍTICO**: Sobreescribir `.card { max-width: none !important; }`
|
||||
- [ ] **CRÍTICO**: Eliminar pseudo-elementos `::before` y `::after` de switches
|
||||
- [ ] **CRÍTICO**: Configurar `background-size: contain` en switches
|
||||
- [ ] **CRÍTICO**: Configurar `background-repeat: no-repeat` en switches
|
||||
- [ ] **CRÍTICO**: Configurar `background-position: left/right center` en switches
|
||||
- [ ] **CRÍTICO**: Implementar `display: flex` + `align-items: center` en `.form-check.form-switch`
|
||||
- [ ] **CRÍTICO**: Configurar `line-height: 16px` en labels de switches
|
||||
- [ ] Usar `.btn` en lugar de `.button`
|
||||
- [ ] Verificar que colores custom tengan `!important` si es necesario
|
||||
- [ ] Usar DevTools para verificar que estilos de WordPress estén overridden
|
||||
- [ ] **Probar switches**: deben mostrar UN solo círculo que se desliza
|
||||
- [ ] **Probar alineación**: labels deben estar perfectamente alineados con switches (0px diferencia)
|
||||
- [ ] Probar en un admin de WordPress real (si aplica)
|
||||
|
||||
---
|
||||
|
||||
## Verificación en DevTools
|
||||
|
||||
### Verificar Fix de Cards
|
||||
|
||||
```css
|
||||
/* ✅ CORRECTO - Deberías ver: */
|
||||
.apus-admin .card {
|
||||
max-width: none !important; /* ✅ Tu CSS */
|
||||
}
|
||||
|
||||
/* Y esto debería estar tachado/overridden: */
|
||||
.card {
|
||||
max-width: 520px; /* ❌ WordPress (overridden) */
|
||||
}
|
||||
```
|
||||
|
||||
### Verificar Fix de Switches
|
||||
|
||||
```css
|
||||
/* ✅ CORRECTO - Deberías ver: */
|
||||
.form-switch .form-check-input[type="checkbox"]::before {
|
||||
content: none !important; /* ✅ Sin checkmark */
|
||||
display: none !important; /* ✅ Oculto */
|
||||
}
|
||||
|
||||
.form-switch .form-check-input[type="checkbox"] {
|
||||
background-size: contain !important; /* ✅ Círculo escalado */
|
||||
background-repeat: no-repeat !important; /* ✅ Sin duplicar */
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Volver al Índice
|
||||
|
||||
[← Volver al README](README.md)
|
||||
420
_planificacion/01-design-system/15-EJEMPLOS-COMPLETOS.md
Normal file
420
_planificacion/01-design-system/15-EJEMPLOS-COMPLETOS.md
Normal file
@@ -0,0 +1,420 @@
|
||||
# 📦 EJEMPLOS COMPLETOS Y RECURSOS
|
||||
|
||||
## Ejemplo 1: Campo de Color con Valor Hex
|
||||
|
||||
```html
|
||||
<div class="col-6">
|
||||
<label for="primaryColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-paint-bucket me-1" style="color: #FF8600;"></i>
|
||||
Color Primario
|
||||
</label>
|
||||
<input type="color" id="primaryColor"
|
||||
class="form-control form-control-color w-100"
|
||||
value="#0E2337"
|
||||
title="Seleccionar color primario">
|
||||
<small class="text-muted d-block mt-1" id="primaryColorValue">#0E2337</small>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const colorInput = document.getElementById('primaryColor');
|
||||
const colorValue = document.getElementById('primaryColorValue');
|
||||
|
||||
colorInput.addEventListener('input', function() {
|
||||
colorValue.textContent = this.value.toUpperCase();
|
||||
updatePreview();
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ejemplo 2: Campo de Texto con Icono de Bootstrap
|
||||
|
||||
```html
|
||||
<div class="mb-2">
|
||||
<label for="iconClass" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-emoji-smile me-1" style="color: #FF8600;"></i>
|
||||
Clase del icono
|
||||
<span class="badge bg-secondary" style="font-size: 0.65rem;">Bootstrap Icons</span>
|
||||
</label>
|
||||
<input type="text" id="iconClass"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="bi bi-star-fill"
|
||||
value="bi bi-megaphone-fill"
|
||||
maxlength="50">
|
||||
<small class="text-muted d-block mt-1">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Ver: <a href="https://icons.getbootstrap.com/" target="_blank"
|
||||
class="text-decoration-none" style="color: #FF8600;">
|
||||
Bootstrap Icons <i class="bi bi-box-arrow-up-right"></i>
|
||||
</a>
|
||||
</small>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ejemplo 3: Textarea con Contador y Progress Bar
|
||||
|
||||
```html
|
||||
<div class="mb-2">
|
||||
<label for="description" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-chat-left-text me-1" style="color: #FF8600;"></i>
|
||||
Descripción <span class="text-danger">*</span>
|
||||
<span class="float-end text-muted">
|
||||
<span id="descriptionCount" class="fw-bold">0</span>/250
|
||||
</span>
|
||||
</label>
|
||||
<textarea id="description"
|
||||
class="form-control form-control-sm"
|
||||
rows="3"
|
||||
maxlength="250"
|
||||
placeholder="Escribe una descripción atractiva..."
|
||||
required></textarea>
|
||||
<div class="progress mt-1" style="height: 3px;">
|
||||
<div id="descriptionProgress"
|
||||
class="progress-bar"
|
||||
role="progressbar"
|
||||
style="width: 0%; background-color: #FF8600;"
|
||||
aria-valuenow="0"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="250"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const textarea = document.getElementById('description');
|
||||
const counter = document.getElementById('descriptionCount');
|
||||
const progress = document.getElementById('descriptionProgress');
|
||||
|
||||
textarea.addEventListener('input', function() {
|
||||
const length = this.value.length;
|
||||
const maxLength = 250;
|
||||
const percentage = (length / maxLength * 100);
|
||||
|
||||
counter.textContent = length;
|
||||
progress.style.width = percentage + '%';
|
||||
progress.setAttribute('aria-valuenow', length);
|
||||
|
||||
// Cambiar color según el uso
|
||||
if (percentage > 90) {
|
||||
progress.style.backgroundColor = '#dc3545'; // Rojo
|
||||
} else if (percentage > 75) {
|
||||
progress.style.backgroundColor = '#ffc107'; // Amarillo
|
||||
} else {
|
||||
progress.style.backgroundColor = '#FF8600'; // Orange
|
||||
}
|
||||
|
||||
updatePreview();
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template Básico Completo
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin: [Component Name]</title>
|
||||
|
||||
<!-- Bootstrap 5.3.2 -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/Css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/Font/bootstrap-icons.min.css">
|
||||
|
||||
<!-- Poppins Font -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- CSS del proyecto principal -->
|
||||
<link rel="stylesheet" href="../../Css/style.css">
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Fix WordPress .card max-width */
|
||||
body .card {
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
/* Fix WordPress switches */
|
||||
body .form-switch .form-check-input[type="checkbox"]::before,
|
||||
body .form-switch .form-check-input[type="checkbox"]::after {
|
||||
content: none !important;
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body .form-switch .form-check-input[type="checkbox"] {
|
||||
background-size: contain !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-position: left center !important;
|
||||
}
|
||||
|
||||
body .form-switch .form-check-input[type="checkbox"]:checked {
|
||||
background-position: right center !important;
|
||||
}
|
||||
|
||||
/* Fix alineación vertical switches */
|
||||
.form-check.form-switch {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.form-switch .form-check-label {
|
||||
line-height: 16px !important;
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 991px) {
|
||||
.tab-header { padding: 0.75rem; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid py-4" style="max-width: 1400px;">
|
||||
<!-- Header del Tab -->
|
||||
<div class="rounded p-4 mb-4 shadow text-white"
|
||||
style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%);
|
||||
border-left: 4px solid #FF8600;">
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
|
||||
<div>
|
||||
<h3 class="h4 mb-1 fw-bold">
|
||||
<i class="bi bi-[icon] me-2" style="color: #FF8600;"></i>
|
||||
Configuración de [Component]
|
||||
</h3>
|
||||
<p class="mb-0 small" style="opacity: 0.85;">
|
||||
[Descripción del componente]
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-light" id="resetDefaults">
|
||||
<i class="bi bi-arrow-counterclockwise me-1"></i>
|
||||
Restaurar valores por defecto
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grid de Contenido -->
|
||||
<div class="row g-3">
|
||||
<!-- Columna Izquierda -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-palette me-2" style="color: #FF8600;"></i>
|
||||
Sección 1
|
||||
</h5>
|
||||
<!-- Campos -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Columna Derecha -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-gear me-2" style="color: #FF8600;"></i>
|
||||
Sección 2
|
||||
</h5>
|
||||
<!-- Campos -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Vista Previa -->
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #FF8600;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-eye me-2" style="color: #FF8600;"></i>
|
||||
Vista Previa en Tiempo Real
|
||||
</h5>
|
||||
|
||||
<div id="componentPreview" class="[component-class]">
|
||||
<!-- HTML idéntico al front-end -->
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||
<small class="text-muted">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Los cambios se reflejan en tiempo real
|
||||
</small>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button type="button" class="btn btn-outline-secondary active" id="previewDesktop">
|
||||
<i class="bi bi-display"></i> Desktop
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" id="previewMobile">
|
||||
<i class="bi bi-phone"></i> Mobile
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/Js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('✅ Admin Panel cargado');
|
||||
loadConfig();
|
||||
updatePreview();
|
||||
initializeEventListeners();
|
||||
});
|
||||
|
||||
function initializeEventListeners() {
|
||||
// Conectar event listeners
|
||||
}
|
||||
|
||||
function updatePreview() {
|
||||
// Actualizar vista previa
|
||||
}
|
||||
|
||||
function saveConfig() {
|
||||
// Guardar configuración
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
// Cargar configuración
|
||||
}
|
||||
|
||||
function resetToDefaults() {
|
||||
if (!confirm('¿Estás seguro de restaurar los valores por defecto?')) {
|
||||
return;
|
||||
}
|
||||
// Reset a defaults
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checklist de Implementación
|
||||
|
||||
### Antes de Empezar
|
||||
|
||||
- [ ] Leer este manual completo
|
||||
- [ ] Revisar la librería de componentes HTML (`../componentes-html-bootstrap/`)
|
||||
- [ ] Identificar qué campos necesita el componente
|
||||
- [ ] Determinar si necesita vista previa en tiempo real
|
||||
|
||||
### Estructura HTML
|
||||
|
||||
- [ ] Crear el header del tab con gradiente navy + border orange
|
||||
- [ ] Incluir botón "Restaurar valores por defecto"
|
||||
- [ ] Organizar campos en grid de 2 columnas (`col-lg-6`)
|
||||
- [ ] Agrupar campos relacionados en cards con border-left navy
|
||||
- [ ] Agregar card de vista previa con border-left orange
|
||||
|
||||
### Formularios
|
||||
|
||||
- [ ] Todos los labels tienen icono orange
|
||||
- [ ] Labels importantes tienen `fw-semibold`
|
||||
- [ ] Campos requeridos tienen `<span class="text-danger">*</span>`
|
||||
- [ ] Color pickers muestran valor hex debajo
|
||||
- [ ] Textareas tienen contador de caracteres + progress bar
|
||||
- [ ] Selects usan `.form-select-sm`
|
||||
- [ ] Inputs usan `.form-control-sm`
|
||||
- [ ] Switches tienen icono + strong en label
|
||||
|
||||
### Vista Previa
|
||||
|
||||
- [ ] HTML de preview es IDÉNTICO al del front-end
|
||||
- [ ] Se usa clase real del componente (ej: `.top-notification-bar`)
|
||||
- [ ] NO hay inline styles que sobreescriban el CSS real
|
||||
- [ ] Se cargan los mismos archivos CSS que el front-end
|
||||
- [ ] Botones Desktop/Mobile funcionales
|
||||
|
||||
### JavaScript
|
||||
|
||||
- [ ] Función `updatePreview()` implementada
|
||||
- [ ] Función `saveConfig()` guarda en localStorage/JSON
|
||||
- [ ] Función `loadConfig()` carga al iniciar
|
||||
- [ ] Función `resetToDefaults()` restaura valores
|
||||
- [ ] Event listeners en todos los campos
|
||||
- [ ] Color pickers actualizan el valor hex
|
||||
- [ ] Textareas actualizan contador + progress bar
|
||||
|
||||
### CSS
|
||||
|
||||
- [ ] Fix WordPress `.card { max-width: none !important; }`
|
||||
- [ ] Fix WordPress switches (pseudo-elementos)
|
||||
- [ ] Fix alineación vertical switches
|
||||
- [ ] Responsive breakpoints configurados
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### La vista previa no se ve igual al front-end
|
||||
|
||||
**Causa:** Inline styles sobreescribiendo el CSS real
|
||||
|
||||
**Solución:**
|
||||
1. Verificar que NO haya inline styles en el HTML del preview
|
||||
2. Verificar que se carga `../../Css/style.css`
|
||||
3. Usar solo la clase del componente real
|
||||
4. Si es necesario, agregar CSS con `!important` SOLO para el border
|
||||
|
||||
### Los colores no coinciden
|
||||
|
||||
**Causa:** No se están usando los valores correctos
|
||||
|
||||
**Solución:**
|
||||
1. Usar valores hex exactos: `#0E2337`, `#FF8600`, etc.
|
||||
2. Verificar que `style.css` define las variables `:root`
|
||||
|
||||
### El contador de caracteres no funciona
|
||||
|
||||
**Causa:** Event listener no conectado
|
||||
|
||||
**Solución:**
|
||||
```javascript
|
||||
textarea.addEventListener('input', function() {
|
||||
counter.textContent = this.value.length;
|
||||
});
|
||||
```
|
||||
|
||||
### El responsive no funciona
|
||||
|
||||
**Causa:** Falta el viewport meta tag
|
||||
|
||||
**Solución:**
|
||||
```html
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Recursos
|
||||
|
||||
### Bootstrap 5.3.2
|
||||
- Documentación: https://getbootstrap.com/docs/5.3/
|
||||
- Components: https://getbootstrap.com/docs/5.3/components/
|
||||
|
||||
### Bootstrap Icons
|
||||
- Catálogo: https://icons.getbootstrap.com/
|
||||
- Búsqueda: Usar el buscador para encontrar iconos
|
||||
|
||||
### Google Fonts - Poppins
|
||||
- Pesos usados: 400 (Regular), 500 (Medium), 600 (Semi-Bold), 700 (Bold)
|
||||
|
||||
---
|
||||
|
||||
## Volver al Índice
|
||||
|
||||
[← Volver al README](README.md)
|
||||
136
_planificacion/01-design-system/README.md
Normal file
136
_planificacion/01-design-system/README.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# 🎨 ADMIN COMPONENTS - DESIGN SYSTEM (MODULARIZADO)
|
||||
|
||||
**Manual de Diseño para Componentes de Administración**
|
||||
Versión 1.0 | APU México
|
||||
|
||||
---
|
||||
|
||||
## 📑 ÍNDICE DE DOCUMENTACIÓN
|
||||
|
||||
Este design system ha sido modularizado en archivos independientes para facilitar su consulta y mantenimiento.
|
||||
|
||||
### 📐 FUNDAMENTOS DEL SISTEMA
|
||||
|
||||
- [**01-ESPECIFICACIONES-DEL-SISTEMA.md**](01-ESPECIFICACIONES-DEL-SISTEMA.md)
|
||||
- Requerimientos generales
|
||||
- Stack tecnológico
|
||||
- Estructura de archivos
|
||||
|
||||
- [**02-FILOSOFIA-DE-DISENO.md**](02-FILOSOFIA-DE-DISENO.md)
|
||||
- Principios clave
|
||||
- Características comunes
|
||||
|
||||
### 🎨 SISTEMA DE DISEÑO VISUAL
|
||||
|
||||
- [**03-PALETA-DE-COLORES.md**](03-PALETA-DE-COLORES.md)
|
||||
- Colores principales (Navy, Orange, Neutral)
|
||||
- Uso de colores por elemento
|
||||
|
||||
- [**04-TIPOGRAFIA.md**](04-TIPOGRAFIA.md)
|
||||
- Font stack (Poppins)
|
||||
- Pesos y tamaños
|
||||
- Jerarquía visual
|
||||
|
||||
- [**05-SISTEMA-GRID-ESPACIADO.md**](05-SISTEMA-GRID-ESPACIADO.md)
|
||||
- Grid de Bootstrap
|
||||
- Breakpoints responsive
|
||||
- Sistema de espaciado (margin/padding)
|
||||
|
||||
### 🏗️ ESTRUCTURA Y COMPONENTES
|
||||
|
||||
- [**06-ESTRUCTURA-LAYOUT.md**](06-ESTRUCTURA-LAYOUT.md)
|
||||
- Container principal
|
||||
- Header del tab (obligatorio)
|
||||
- Sistema de grid 2 columnas
|
||||
|
||||
- [**07-COMPONENTES-REUTILIZABLES.md**](07-COMPONENTES-REUTILIZABLES.md)
|
||||
- Botones (primario, secundario, reset)
|
||||
- Inputs (text, color, textarea, select)
|
||||
- Cards (estándar, vista previa)
|
||||
|
||||
- [**08-PATRONES-FORMULARIOS.md**](08-PATRONES-FORMULARIOS.md)
|
||||
- Form switches
|
||||
- Color pickers
|
||||
- Text inputs con contador
|
||||
- Badges y links de ayuda
|
||||
|
||||
### 🔄 FUNCIONALIDAD Y COMPORTAMIENTO
|
||||
|
||||
- [**09-VISTA-PREVIA-TIEMPO-REAL.md**](09-VISTA-PREVIA-TIEMPO-REAL.md)
|
||||
- Estructura HTML del preview
|
||||
- Reglas críticas
|
||||
- CSS para vista previa
|
||||
|
||||
- [**10-RESPONSIVE-DESIGN.md**](10-RESPONSIVE-DESIGN.md)
|
||||
- Breakpoints
|
||||
- Patrones responsive
|
||||
- Media queries
|
||||
|
||||
- [**11-JAVASCRIPT-PATTERNS.md**](11-JAVASCRIPT-PATTERNS.md)
|
||||
- Inicialización
|
||||
- Event listeners
|
||||
- updatePreview()
|
||||
- Guardar/cargar configuración
|
||||
|
||||
- [**12-PERSISTENCIA-JSON.md**](12-PERSISTENCIA-JSON.md)
|
||||
- Estructura del config.json
|
||||
- Funciones de persistencia
|
||||
- Sistema de notificaciones
|
||||
|
||||
### 🎛️ PANEL Y ADMINISTRACIÓN
|
||||
|
||||
- [**13-PANEL-ADMINISTRACION.md**](13-PANEL-ADMINISTRACION.md)
|
||||
- Estructura del panel principal
|
||||
- Grid de componentes
|
||||
- Patrón para nuevos componentes
|
||||
|
||||
- [**14-CONFLICTOS-WORDPRESS.md**](14-CONFLICTOS-WORDPRESS.md)
|
||||
- Conflicto con .card
|
||||
- Conflicto con form switches
|
||||
- Conflicto con alineación vertical
|
||||
- Checklist anti-conflictos
|
||||
|
||||
### 📚 RECURSOS Y EJEMPLOS
|
||||
|
||||
- [**15-EJEMPLOS-COMPLETOS.md**](15-EJEMPLOS-COMPLETOS.md)
|
||||
- Ejemplos de campos completos
|
||||
- Template básico
|
||||
- Checklist de implementación
|
||||
- Troubleshooting
|
||||
|
||||
### 🎨 TOKENS DE DISEÑO
|
||||
|
||||
- [**design-tokens-apus-admin.scss**](design-tokens-apus-admin.scss)
|
||||
- Variables SCSS para colores
|
||||
- Espaciado
|
||||
- Tipografía
|
||||
- Sombras y bordes
|
||||
|
||||
---
|
||||
|
||||
## 🚀 QUICK START
|
||||
|
||||
1. **Lee primero:** [01-ESPECIFICACIONES-DEL-SISTEMA.md](01-ESPECIFICACIONES-DEL-SISTEMA.md)
|
||||
2. **Consulta el diseño:** [03-PALETA-DE-COLORES.md](03-PALETA-DE-COLORES.md) y [04-TIPOGRAFIA.md](04-TIPOGRAFIA.md)
|
||||
3. **Usa los componentes:** [07-COMPONENTES-REUTILIZABLES.md](07-COMPONENTES-REUTILIZABLES.md)
|
||||
4. **Implementa JavaScript:** [11-JAVASCRIPT-PATTERNS.md](11-JAVASCRIPT-PATTERNS.md)
|
||||
5. **Resuelve conflictos WP:** [14-CONFLICTOS-WORDPRESS.md](14-CONFLICTOS-WORDPRESS.md)
|
||||
|
||||
---
|
||||
|
||||
## 📝 NOTAS IMPORTANTES
|
||||
|
||||
1. ✅ **Usar la librería de componentes HTML como referencia** (`../componentes-html-bootstrap/`)
|
||||
2. ❌ **NUNCA modificar el HTML del front-end desde el admin panel**
|
||||
3. ✅ **La vista previa debe ser 100% idéntica al componente real**
|
||||
4. ✅ **Guardar configuración en archivos JSON (NO localStorage)**
|
||||
5. ✅ **Responsive es obligatorio, no opcional**
|
||||
6. ✅ **Todos los iconos deben ser orange (#FF8600)**
|
||||
7. ✅ **Todos los títulos de card deben ser navy (#1e3a5f)**
|
||||
|
||||
---
|
||||
|
||||
**Documento creado:** 2025
|
||||
**Versión:** 1.0
|
||||
**Proyecto:** APU México - Sistema de Administración
|
||||
**Autor:** Design System Team
|
||||
358
_planificacion/01-design-system/design-tokens-apus-admin.scss
Normal file
358
_planificacion/01-design-system/design-tokens-apus-admin.scss
Normal file
@@ -0,0 +1,358 @@
|
||||
// =============================================================================
|
||||
// APUS ADMIN PANEL - DESIGN TOKENS
|
||||
// =============================================================================
|
||||
// Este archivo contiene todas las variables de diseño del sistema de
|
||||
// administración de APU México.
|
||||
//
|
||||
// Uso:
|
||||
// @import 'design-tokens-apus-admin';
|
||||
// =============================================================================
|
||||
|
||||
// =============================================================================
|
||||
// COLORES
|
||||
// =============================================================================
|
||||
|
||||
// Navy Brand Colors
|
||||
// -----------------------------------------------------------------------------
|
||||
$color-navy-dark: #0E2337; // Fondo principal, headers
|
||||
$color-navy-primary: #1e3a5f; // Títulos, bordes importantes
|
||||
$color-navy-light: #2c5282; // Variaciones secundarias
|
||||
|
||||
// Orange Accent Colors
|
||||
// -----------------------------------------------------------------------------
|
||||
$color-orange-primary: #FF8600; // Acción primaria, iconos destacados
|
||||
$color-orange-hover: #FF6B35; // Hover states
|
||||
$color-orange-light: #FFB800; // Badges, alerts suaves
|
||||
|
||||
// Neutral Colors
|
||||
// -----------------------------------------------------------------------------
|
||||
$color-neutral-50: #f8f9fa; // Fondo general del body
|
||||
$color-neutral-100: #e9ecef; // Bordes, separadores
|
||||
$color-neutral-200: #dee2e6; // Bordes sutiles
|
||||
$color-neutral-600: #495057; // Texto principal
|
||||
$color-neutral-700: #6c757d; // Texto secundario
|
||||
$color-neutral-900: #212529; // Texto muy oscuro
|
||||
|
||||
// Semantic Colors
|
||||
// -----------------------------------------------------------------------------
|
||||
$color-success: #28a745; // Acciones exitosas
|
||||
$color-danger: #dc3545; // Errores, acciones destructivas
|
||||
$color-warning: #ffc107; // Advertencias
|
||||
$color-info: #17a2b8; // Información
|
||||
|
||||
// Background Colors
|
||||
// -----------------------------------------------------------------------------
|
||||
$bg-body: $color-neutral-50;
|
||||
$bg-card: #ffffff;
|
||||
$bg-header: linear-gradient(135deg, $color-navy-dark 0%, $color-navy-primary 100%);
|
||||
|
||||
// =============================================================================
|
||||
// TIPOGRAFÍA
|
||||
// =============================================================================
|
||||
|
||||
// Font Family
|
||||
// -----------------------------------------------------------------------------
|
||||
$font-family-base: 'Poppins', sans-serif;
|
||||
|
||||
// Font Sizes
|
||||
// -----------------------------------------------------------------------------
|
||||
$font-size-base: 1rem; // 16px
|
||||
$font-size-sm: 0.875rem; // 14px (labels, small text)
|
||||
$font-size-xs: 0.75rem; // 12px (hints, badges)
|
||||
|
||||
$font-size-h1: 2rem; // 32px
|
||||
$font-size-h2: 1.75rem; // 28px
|
||||
$font-size-h3: 1.5rem; // 24px (título principal del tab)
|
||||
$font-size-h4: 1.25rem; // 20px (subtítulos grandes)
|
||||
$font-size-h5: 1rem; // 16px (títulos de cards)
|
||||
$font-size-h6: 0.875rem; // 14px
|
||||
|
||||
// Font Weights
|
||||
// -----------------------------------------------------------------------------
|
||||
$font-weight-light: 300;
|
||||
$font-weight-normal: 400;
|
||||
$font-weight-medium: 500;
|
||||
$font-weight-semibold: 600;
|
||||
$font-weight-bold: 700;
|
||||
|
||||
// Line Heights
|
||||
// -----------------------------------------------------------------------------
|
||||
$line-height-base: 1.5;
|
||||
$line-height-tight: 1.2; // Para títulos
|
||||
$line-height-relaxed: 1.6;
|
||||
|
||||
// =============================================================================
|
||||
// ESPACIADO
|
||||
// =============================================================================
|
||||
|
||||
// Spacing Scale (rem)
|
||||
// -----------------------------------------------------------------------------
|
||||
$spacing-0: 0;
|
||||
$spacing-1: 0.25rem; // 4px
|
||||
$spacing-2: 0.5rem; // 8px
|
||||
$spacing-3: 1rem; // 16px
|
||||
$spacing-4: 1.5rem; // 24px
|
||||
$spacing-5: 3rem; // 48px
|
||||
|
||||
// Specific Usage
|
||||
// -----------------------------------------------------------------------------
|
||||
$spacing-field-margin: $spacing-2; // mb-2 en campos
|
||||
$spacing-card-margin: $spacing-3; // mb-3 en cards
|
||||
$spacing-header-margin: $spacing-4; // mb-4 en headers
|
||||
$spacing-icon-margin: $spacing-2; // me-2 en iconos grandes
|
||||
$spacing-icon-small-margin: $spacing-1; // me-1 en iconos pequeños
|
||||
$spacing-card-padding: $spacing-3; // p-3 en card-body
|
||||
$spacing-container-padding: $spacing-4; // py-4 en container
|
||||
|
||||
// =============================================================================
|
||||
// LAYOUT
|
||||
// =============================================================================
|
||||
|
||||
// Container
|
||||
// -----------------------------------------------------------------------------
|
||||
$container-max-width: 1400px;
|
||||
$container-padding-x: 0.75rem;
|
||||
|
||||
// Grid
|
||||
// -----------------------------------------------------------------------------
|
||||
$grid-gutter-width: 1.5rem;
|
||||
$grid-gutter-sm: 1rem; // g-3
|
||||
$grid-gutter-xs: 0.5rem; // g-2
|
||||
|
||||
// Breakpoints
|
||||
// -----------------------------------------------------------------------------
|
||||
$breakpoint-xs: 0;
|
||||
$breakpoint-sm: 576px;
|
||||
$breakpoint-md: 768px;
|
||||
$breakpoint-lg: 992px; // Punto de quiebre principal
|
||||
$breakpoint-xl: 1200px;
|
||||
$breakpoint-xxl: 1400px;
|
||||
|
||||
// =============================================================================
|
||||
// BORDES
|
||||
// =============================================================================
|
||||
|
||||
// Border Width
|
||||
// -----------------------------------------------------------------------------
|
||||
$border-width-thin: 1px;
|
||||
$border-width-thick: 4px;
|
||||
|
||||
// Border Radius
|
||||
// -----------------------------------------------------------------------------
|
||||
$border-radius-sm: 0.25rem; // 4px
|
||||
$border-radius: 0.375rem; // 6px (default Bootstrap)
|
||||
$border-radius-lg: 0.5rem; // 8px
|
||||
|
||||
// Border Colors
|
||||
// -----------------------------------------------------------------------------
|
||||
$border-color: $color-neutral-200;
|
||||
$border-color-card: $color-navy-primary;
|
||||
$border-color-preview: $color-orange-primary;
|
||||
|
||||
// =============================================================================
|
||||
// SOMBRAS
|
||||
// =============================================================================
|
||||
|
||||
$shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
$shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
$shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
|
||||
|
||||
// =============================================================================
|
||||
// COMPONENTES
|
||||
// =============================================================================
|
||||
|
||||
// Cards
|
||||
// -----------------------------------------------------------------------------
|
||||
$card-border-width: 0;
|
||||
$card-border-left-width: $border-width-thick;
|
||||
$card-border-radius: $border-radius;
|
||||
$card-shadow: $shadow-sm;
|
||||
$card-padding: $spacing-card-padding;
|
||||
$card-margin: $spacing-card-margin;
|
||||
|
||||
// Buttons
|
||||
// -----------------------------------------------------------------------------
|
||||
$btn-border-radius: $border-radius;
|
||||
$btn-padding-y: 0.375rem;
|
||||
$btn-padding-x: 0.75rem;
|
||||
$btn-padding-y-sm: 0.25rem;
|
||||
$btn-padding-x-sm: 0.5rem;
|
||||
|
||||
// Inputs
|
||||
// -----------------------------------------------------------------------------
|
||||
$input-border-radius: $border-radius;
|
||||
$input-border-color: $color-neutral-300;
|
||||
$input-focus-border-color: $color-orange-primary;
|
||||
$input-padding-y: 0.375rem;
|
||||
$input-padding-x: 0.75rem;
|
||||
$input-padding-y-sm: 0.25rem;
|
||||
$input-padding-x-sm: 0.5rem;
|
||||
|
||||
// Badges
|
||||
// -----------------------------------------------------------------------------
|
||||
$badge-font-size: 0.65rem;
|
||||
$badge-padding-y: 0.25rem;
|
||||
$badge-padding-x: 0.5rem;
|
||||
|
||||
// Progress Bar
|
||||
// -----------------------------------------------------------------------------
|
||||
$progress-height: 3px;
|
||||
$progress-bar-bg: $color-orange-primary;
|
||||
$progress-bar-warning: $color-warning;
|
||||
$progress-bar-danger: $color-danger;
|
||||
|
||||
// =============================================================================
|
||||
// TRANSICIONES
|
||||
// =============================================================================
|
||||
|
||||
$transition-base: all 0.3s ease-in-out;
|
||||
$transition-fast: all 0.15s ease-in-out;
|
||||
$transition-slow: all 0.5s ease-in-out;
|
||||
|
||||
// =============================================================================
|
||||
// Z-INDEX
|
||||
// =============================================================================
|
||||
|
||||
$z-index-notification: 9999;
|
||||
$z-index-modal: 1050;
|
||||
$z-index-dropdown: 1000;
|
||||
$z-index-sticky: 1020;
|
||||
|
||||
// =============================================================================
|
||||
// MIXINS
|
||||
// =============================================================================
|
||||
|
||||
// Media Query Mixins
|
||||
// -----------------------------------------------------------------------------
|
||||
@mixin media-xs {
|
||||
@media (max-width: #{$breakpoint-sm - 1px}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin media-sm {
|
||||
@media (min-width: $breakpoint-sm) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin media-md {
|
||||
@media (min-width: $breakpoint-md) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin media-lg {
|
||||
@media (min-width: $breakpoint-lg) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin media-xl {
|
||||
@media (min-width: $breakpoint-xl) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin media-xxl {
|
||||
@media (min-width: $breakpoint-xxl) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
// Component Mixins
|
||||
// -----------------------------------------------------------------------------
|
||||
@mixin card-base {
|
||||
border: $card-border-width;
|
||||
border-radius: $card-border-radius;
|
||||
box-shadow: $card-shadow;
|
||||
background-color: $bg-card;
|
||||
margin-bottom: $card-margin;
|
||||
}
|
||||
|
||||
@mixin card-border-left($color: $border-color-card) {
|
||||
border-left: $border-width-thick solid $color;
|
||||
}
|
||||
|
||||
@mixin text-truncate {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@mixin center-flex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// UTILITY CLASSES (Opcional - si no usas Bootstrap utilities)
|
||||
// =============================================================================
|
||||
|
||||
// Solo descomentar si necesitas clases que no están en Bootstrap
|
||||
|
||||
/*
|
||||
.text-navy {
|
||||
color: $color-navy-primary;
|
||||
}
|
||||
|
||||
.text-orange {
|
||||
color: $color-orange-primary;
|
||||
}
|
||||
|
||||
.bg-navy {
|
||||
background-color: $color-navy-primary;
|
||||
}
|
||||
|
||||
.bg-orange {
|
||||
background-color: $color-orange-primary;
|
||||
}
|
||||
|
||||
.border-navy {
|
||||
border-color: $color-navy-primary !important;
|
||||
}
|
||||
|
||||
.border-orange {
|
||||
border-color: $color-orange-primary !important;
|
||||
}
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// EJEMPLO DE USO
|
||||
// =============================================================================
|
||||
|
||||
/*
|
||||
// En tu archivo SCSS:
|
||||
@import 'design-tokens-apus-admin';
|
||||
|
||||
.custom-header {
|
||||
background: $bg-header;
|
||||
color: white;
|
||||
padding: $spacing-card-padding;
|
||||
border-left: $border-width-thick solid $color-orange-primary;
|
||||
|
||||
h1 {
|
||||
font-size: $font-size-h3;
|
||||
font-weight: $font-weight-bold;
|
||||
margin-bottom: $spacing-1;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-card {
|
||||
@include card-base;
|
||||
@include card-border-left($color-navy-primary);
|
||||
|
||||
&.preview {
|
||||
@include card-border-left($color-orange-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.responsive-text {
|
||||
font-size: $font-size-sm;
|
||||
|
||||
@include media-lg {
|
||||
font-size: $font-size-base;
|
||||
}
|
||||
}
|
||||
*/
|
||||
341
_planificacion/analisis-spam-formularios.md
Normal file
341
_planificacion/analisis-spam-formularios.md
Normal file
@@ -0,0 +1,341 @@
|
||||
# Análisis de Spam en Formularios - ROI Theme
|
||||
|
||||
**Fecha de análisis:** 2026-01-08
|
||||
**Problema:** Recepción de spam con datos aleatorios en formularios
|
||||
|
||||
## Características del Spam Detectado
|
||||
|
||||
- **Nombres:** Cadenas aleatorias (ej: `kxUcwkDPHRAnUbdRWnDx`, `SOTbwKzTcZhJfTRBYSTV`)
|
||||
- **WhatsApp:** Cadenas aleatorias en lugar de números
|
||||
- **Emails:** Emails reales (posiblemente robados de bases de datos)
|
||||
- **Patrón:** Bots automatizados que llenan campos sin validación
|
||||
- **Fuente identificada:** `newsletter-footer`
|
||||
|
||||
## Módulos de Formularios Identificados
|
||||
|
||||
### Formulario 1: Newsletter Footer
|
||||
- **Handler**: `Public/Footer/Infrastructure/Api/WordPress/NewsletterAjaxHandler.php`
|
||||
- **Renderer**: `Public/Footer/Infrastructure/Ui/FooterRenderer.php`
|
||||
- **Acción AJAX**: `roi_newsletter_subscribe`
|
||||
- **Source en payload**: `newsletter-footer`
|
||||
|
||||
### Formulario 2: Contact Form (Sección + Modal)
|
||||
- **Handler**: `Public/ContactForm/Infrastructure/Api/WordPress/ContactFormAjaxHandler.php`
|
||||
- **Renderer**: `Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php`
|
||||
- **Acción AJAX**: `roi_contact_form_submit`
|
||||
- **Source en payload**: `contact-form`
|
||||
|
||||
---
|
||||
|
||||
## Hallazgos Durante la Revisión
|
||||
|
||||
### Archivos Identificados
|
||||
|
||||
**Schemas:**
|
||||
- `Schemas/contact-form.json`
|
||||
|
||||
**JavaScript (Frontend):**
|
||||
- `Assets/Js/footer-contact.js`
|
||||
- `Assets/Js/modal-contact.js`
|
||||
|
||||
**PHP Backend - Newsletter (FUENTE DEL SPAM):**
|
||||
- `Public/Footer/Infrastructure/Api/WordPress/NewsletterAjaxHandler.php`
|
||||
- `Public/Footer/Infrastructure/Ui/FooterRenderer.php`
|
||||
|
||||
---
|
||||
|
||||
## Análisis del Formulario Newsletter (PRINCIPAL AFECTADO)
|
||||
|
||||
### Ubicación
|
||||
- **Handler AJAX**: `Public/Footer/Infrastructure/Api/WordPress/NewsletterAjaxHandler.php`
|
||||
- **Renderer HTML/JS**: `Public/Footer/Infrastructure/Ui/FooterRenderer.php`
|
||||
- **Fuente en payload**: `newsletter-footer` ✓ (coincide con spam reportado)
|
||||
|
||||
### Medidas de Seguridad EXISTENTES ✅
|
||||
|
||||
| Medida | Estado | Archivo | Línea |
|
||||
|--------|--------|---------|-------|
|
||||
| Nonce WordPress | ✅ Implementado | NewsletterAjaxHandler.php | 46 |
|
||||
| Rate Limiting (60s por IP) | ✅ Implementado | NewsletterAjaxHandler.php | 54-59 |
|
||||
| Sanitización de inputs | ✅ Implementado | NewsletterAjaxHandler.php | 62-64 |
|
||||
| Validación de email | ✅ Implementado | NewsletterAjaxHandler.php | 66 |
|
||||
| Webhook URL oculta | ✅ Implementado | Nunca expuesta al cliente |
|
||||
|
||||
### VULNERABILIDADES CRÍTICAS ❌
|
||||
|
||||
| Vulnerabilidad | Impacto | Prioridad |
|
||||
|----------------|---------|-----------|
|
||||
| **NO hay honeypot** | Bots pasan sin detección | 🔴 ALTA |
|
||||
| **NO hay CAPTCHA** | Sin verificación humana | 🔴 ALTA |
|
||||
| **NO hay validación de nombre** | Acepta `kxUcwkDPHRAnUbdRWnDx` | 🔴 ALTA |
|
||||
| **NO hay validación de WhatsApp** | Acepta `OGkrLENXqiQAaIYvCV` | 🔴 ALTA |
|
||||
| **Rate limiting débil** | 60s es poco, bots rotan IPs | 🟡 MEDIA |
|
||||
| **NO hay tiempo mínimo de envío** | Bots envían instantáneamente | 🟡 MEDIA |
|
||||
|
||||
### Campos del Formulario (FooterRenderer.php:336-343)
|
||||
|
||||
```html
|
||||
<form id="roi-newsletter-form">
|
||||
<input type="hidden" name="nonce" value="...">
|
||||
<input type="text" name="name" placeholder="Nombre"> <!-- SIN VALIDACIÓN -->
|
||||
<input type="email" name="email" placeholder="Email" required> <!-- SOLO HTML5 -->
|
||||
<input type="tel" name="whatsapp" placeholder="WhatsApp"> <!-- SIN VALIDACIÓN -->
|
||||
<button type="submit">Suscribirse</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
### Patrón del Spam Detectado
|
||||
|
||||
Los datos de spam muestran:
|
||||
- **Nombres**: Cadenas alfanuméricas aleatorias (20+ caracteres)
|
||||
- **WhatsApp**: Cadenas alfanuméricas aleatorias (NO son números)
|
||||
- **Emails**: Emails reales (posiblemente de bases de datos filtradas)
|
||||
|
||||
Esto indica **bots automatizados** que:
|
||||
1. Bypassean la validación HTML5 (trivial)
|
||||
2. Ignoran el rate limiting rotando IPs
|
||||
3. Generan valores aleatorios para campos de texto
|
||||
4. Usan emails reales para parecer legítimos
|
||||
|
||||
---
|
||||
|
||||
## Análisis del Formulario Contact Form (SEGUNDO FORMULARIO)
|
||||
|
||||
### Ubicación
|
||||
- **Handler AJAX**: `Public/ContactForm/Infrastructure/Api/WordPress/ContactFormAjaxHandler.php`
|
||||
- **Renderer**: `Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php`
|
||||
- **Schema**: `Schemas/contact-form.json`
|
||||
- **Fuente en payload**: `contact-form`
|
||||
|
||||
### Estado: ✅ ACTIVO (Clean Architecture)
|
||||
|
||||
Este formulario está implementado correctamente con Clean Architecture:
|
||||
- Renderer genera HTML/CSS/JS dinámicamente
|
||||
- Handler AJAX procesa envíos server-side
|
||||
- Webhook URL guardada en BD (nunca expuesta al cliente)
|
||||
|
||||
### Medidas de Seguridad EXISTENTES ✅
|
||||
|
||||
| Medida | Estado | Archivo | Línea |
|
||||
|--------|--------|---------|-------|
|
||||
| Nonce WordPress | ✅ Implementado | ContactFormAjaxHandler.php | 47-48 |
|
||||
| Rate Limiting (30s por IP) | ✅ Implementado | ContactFormAjaxHandler.php | 55-61 |
|
||||
| Sanitización de inputs | ✅ Implementado | ContactFormAjaxHandler.php | 122-130 |
|
||||
| Validación de email | ✅ Implementado | ContactFormAjaxHandler.php | 151-155 |
|
||||
| Webhook URL oculta | ✅ Implementado | ContactFormAjaxHandler.php | 86 |
|
||||
|
||||
### VULNERABILIDADES CRÍTICAS ❌
|
||||
|
||||
| Vulnerabilidad | Impacto | Prioridad |
|
||||
|----------------|---------|-----------|
|
||||
| **NO hay honeypot** | Bots pasan sin detección | 🔴 ALTA |
|
||||
| **NO hay CAPTCHA** | Sin verificación humana | 🔴 ALTA |
|
||||
| **NO hay validación de nombre** | Acepta `kxUcwkDPHRAnUbdRWnDx` | 🔴 ALTA |
|
||||
| **NO hay validación de WhatsApp** | Acepta `OGkrLENXqiQAaIYvCV` | 🔴 ALTA |
|
||||
| **Rate limiting muy débil** | Solo 30s, bots rotan IPs | 🟡 MEDIA |
|
||||
| **NO hay tiempo mínimo de envío** | Bots envían instantáneamente | 🟡 MEDIA |
|
||||
|
||||
### Campos del Formulario (ContactFormRenderer.php:278-320)
|
||||
|
||||
```html
|
||||
<form id="roiContactForm" data-nonce="...">
|
||||
<input type="text" name="fullName" placeholder="Nombre completo *" required> <!-- SIN VALIDACIÓN FORMATO -->
|
||||
<input type="text" name="company" placeholder="Empresa">
|
||||
<input type="tel" name="whatsapp" placeholder="WhatsApp *" required> <!-- SIN VALIDACIÓN FORMATO -->
|
||||
<input type="email" name="email" placeholder="Correo electrónico *" required> <!-- SOLO required + is_email -->
|
||||
<textarea name="message"></textarea>
|
||||
</form>
|
||||
```
|
||||
|
||||
### Nota sobre archivo Legacy (footer-contact.js)
|
||||
|
||||
El archivo `Assets/Js/footer-contact.js` es **código legacy NO utilizado**:
|
||||
- NO está encolado en `Inc/enqueue-scripts.php`
|
||||
- Tiene webhook URL hardcodeada (mala práctica)
|
||||
- Fue reemplazado por el sistema Clean Architecture actual
|
||||
|
||||
---
|
||||
|
||||
## Resumen Comparativo de Ambos Formularios
|
||||
|
||||
| Característica | Newsletter Footer | Contact Form |
|
||||
|----------------|-------------------|--------------|
|
||||
| **Estado** | ✅ ACTIVO | ✅ ACTIVO |
|
||||
| **Backend** | PHP AJAX | PHP AJAX |
|
||||
| **Nonce** | ✅ Sí | ✅ Sí |
|
||||
| **Rate Limit** | ✅ 60s | ✅ 30s |
|
||||
| **Sanitización** | ✅ Sí | ✅ Sí |
|
||||
| **Validación email** | ✅ is_email() | ✅ is_email() |
|
||||
| **Validación nombre** | ❌ Ninguna | ❌ Solo required |
|
||||
| **Validación WhatsApp** | ❌ Ninguna | ❌ Solo required |
|
||||
| **Honeypot** | ❌ NO | ❌ NO |
|
||||
| **CAPTCHA** | ❌ NO | ❌ NO |
|
||||
| **Tiempo mínimo** | ❌ NO | ❌ NO |
|
||||
|
||||
### Conclusión: AMBOS formularios comparten las MISMAS vulnerabilidades críticas
|
||||
|
||||
---
|
||||
|
||||
## Soluciones Anti-Spam Propuestas
|
||||
|
||||
### PRIORIDAD 1: Newsletter Footer (Urgente)
|
||||
|
||||
#### 1.1 Honeypot Field (Implementación rápida, efectiva)
|
||||
```html
|
||||
<!-- Campo oculto que humanos no llenan pero bots sí -->
|
||||
<input type="text" name="website_url" style="display:none !important" tabindex="-1" autocomplete="off">
|
||||
```
|
||||
|
||||
Backend (`NewsletterAjaxHandler.php`):
|
||||
```php
|
||||
// Verificar honeypot
|
||||
$honeypot = sanitize_text_field($_POST['website_url'] ?? '');
|
||||
if (!empty($honeypot)) {
|
||||
// Es bot - responder éxito falso para no alertar
|
||||
wp_send_json_success(['message' => $successMsg]);
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2 Validación de Contenido (Anti-gibberish)
|
||||
```php
|
||||
// Validar nombre: solo letras, espacios, acentos
|
||||
$name = sanitize_text_field($_POST['name'] ?? '');
|
||||
if (!empty($name) && !preg_match('/^[\p{L}\s\'-]{2,50}$/u', $name)) {
|
||||
wp_send_json_error(['message' => 'Nombre inválido'], 422);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validar WhatsApp: solo números, +, -, espacios
|
||||
$whatsapp = sanitize_text_field($_POST['whatsapp'] ?? '');
|
||||
if (!empty($whatsapp) && !preg_match('/^[\d\s\+\-\(\)]{10,20}$/', $whatsapp)) {
|
||||
wp_send_json_error(['message' => 'Número de WhatsApp inválido'], 422);
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.3 Tiempo Mínimo de Envío
|
||||
```php
|
||||
// En el formulario HTML agregar:
|
||||
<input type="hidden" name="form_timestamp" value="<?php echo time(); ?>">
|
||||
|
||||
// En el handler:
|
||||
$timestamp = (int) ($_POST['form_timestamp'] ?? 0);
|
||||
$minTime = 3; // segundos mínimos
|
||||
if (time() - $timestamp < $minTime) {
|
||||
// Envío demasiado rápido = bot
|
||||
wp_send_json_success(['message' => $successMsg]); // Falso éxito
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.4 Rate Limiting Mejorado
|
||||
```php
|
||||
// Aumentar de 60s a 300s (5 minutos)
|
||||
// Y agregar límite diario por IP
|
||||
private function checkRateLimit(): bool
|
||||
{
|
||||
$ip = $this->getClientIP();
|
||||
|
||||
// Límite corto (5 minutos)
|
||||
$shortKey = 'roi_newsletter_short_' . md5($ip);
|
||||
if (get_transient($shortKey) !== false) {
|
||||
return false;
|
||||
}
|
||||
set_transient($shortKey, time(), 300);
|
||||
|
||||
// Límite diario (máximo 5 por día)
|
||||
$dailyKey = 'roi_newsletter_daily_' . md5($ip);
|
||||
$dailyCount = (int) get_transient($dailyKey);
|
||||
if ($dailyCount >= 5) {
|
||||
return false;
|
||||
}
|
||||
set_transient($dailyKey, $dailyCount + 1, DAY_IN_SECONDS);
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### PRIORIDAD 2: Opciones Adicionales
|
||||
|
||||
#### 2.1 Google reCAPTCHA v3 (Invisible)
|
||||
- Sin fricción para usuarios
|
||||
- Score basado en comportamiento
|
||||
- Requiere cuenta Google reCAPTCHA
|
||||
|
||||
#### 2.2 Cloudflare Turnstile (Recomendado)
|
||||
- Gratuito
|
||||
- Sin tracking de Google
|
||||
- Más privado que reCAPTCHA
|
||||
|
||||
#### 2.3 hCaptcha
|
||||
- Alternativa a reCAPTCHA
|
||||
- Mejor privacidad
|
||||
- Versión gratuita disponible
|
||||
|
||||
---
|
||||
|
||||
## Plan de Implementación
|
||||
|
||||
### Fase 1: Crear Servicio Anti-Spam Compartido
|
||||
1. Crear `Shared/Infrastructure/Services/AntiSpamValidator.php`
|
||||
2. Implementar validaciones reutilizables:
|
||||
- `validateHoneypot(string $value): bool`
|
||||
- `validateMinimumTime(int $timestamp, int $minSeconds = 3): bool`
|
||||
- `validateNameFormat(string $name): bool` (regex letras/espacios)
|
||||
- `validateWhatsAppFormat(string $phone): bool` (regex números)
|
||||
- `checkEnhancedRateLimit(string $ip, int $shortLimit, int $dailyLimit): bool`
|
||||
|
||||
### Fase 2: Aplicar a Newsletter Footer
|
||||
1. Agregar honeypot + timestamp en `FooterRenderer.php`
|
||||
2. Integrar AntiSpamValidator en `NewsletterAjaxHandler.php`
|
||||
3. Probar que spam es rechazado
|
||||
|
||||
### Fase 3: Aplicar a Contact Form
|
||||
1. Agregar honeypot + timestamp en `ContactFormRenderer.php`
|
||||
2. Integrar AntiSpamValidator en `ContactFormAjaxHandler.php`
|
||||
3. Probar que spam es rechazado
|
||||
|
||||
### Fase 4: Monitoreo y Mejoras (Opcional)
|
||||
1. Agregar logging de intentos sospechosos
|
||||
2. Evaluar CAPTCHA invisible si spam persiste
|
||||
3. Dashboard de estadísticas anti-spam
|
||||
|
||||
---
|
||||
|
||||
## Archivos a Modificar
|
||||
|
||||
### Newsletter Footer
|
||||
| Archivo | Cambio |
|
||||
|---------|--------|
|
||||
| `Public/Footer/Infrastructure/Ui/FooterRenderer.php` | Agregar honeypot + timestamp oculto |
|
||||
| `Public/Footer/Infrastructure/Api/WordPress/NewsletterAjaxHandler.php` | Validaciones formato + honeypot check + rate limiting mejorado |
|
||||
|
||||
### Contact Form
|
||||
| Archivo | Cambio |
|
||||
|---------|--------|
|
||||
| `Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php` | Agregar honeypot + timestamp oculto |
|
||||
| `Public/ContactForm/Infrastructure/Api/WordPress/ContactFormAjaxHandler.php` | Validaciones formato + honeypot check + rate limiting mejorado |
|
||||
|
||||
### Código Reutilizable (Recomendado)
|
||||
Crear un trait o clase helper compartida para evitar duplicación:
|
||||
```
|
||||
Shared/Infrastructure/Services/AntiSpamValidator.php
|
||||
```
|
||||
- Validación de honeypot
|
||||
- Validación de tiempo mínimo
|
||||
- Validación de formato nombre (regex)
|
||||
- Validación de formato WhatsApp (regex)
|
||||
- Rate limiting mejorado
|
||||
|
||||
---
|
||||
|
||||
## Referencias Serena
|
||||
|
||||
Las memorias existentes en `.serena/Memories/` documentan:
|
||||
- `diagnostico-estructura-carpetas-admin.md` - Estructura de carpetas Admin
|
||||
- `migracion-theme-options-tabla-personalizada.md` - Sistema de settings
|
||||
- `ROI_THEME_CSS_LOADING_ANALYSIS.md` - Análisis de carga CSS
|
||||
|
||||
Esta memoria debe guardarse como referencia para implementación futura.
|
||||
|
||||
366
_planificacion/plan-mejora-especificaciones-openspec.md
Normal file
366
_planificacion/plan-mejora-especificaciones-openspec.md
Normal file
@@ -0,0 +1,366 @@
|
||||
# Plan de Mejora de Especificaciones OpenSpec - ROI Theme
|
||||
|
||||
**Fecha:** 2026-01-08
|
||||
**Referencia:** Modelo de especificaciones de Aditec (`D:\_Desarrollo\100Aditec\_openspec`)
|
||||
|
||||
---
|
||||
|
||||
## INSTRUCCIONES DE RECUPERACIÓN (LEER PRIMERO)
|
||||
|
||||
> **Si la conversación se compactó, se fue el internet o se apagó la computadora:**
|
||||
|
||||
1. **Estado actual:** Ver sección "ESTADO ACTUAL" abajo
|
||||
2. **Buscar primer `[ ]`:** Ir a la tarea sin marcar
|
||||
3. **Continuar desde ahí:** No repetir tareas marcadas `[x]`
|
||||
4. **Marcar `[x]` al completar:** Guardar después de cada cambio
|
||||
|
||||
### ESTADO ACTUAL
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **Última actualización** | 2026-01-08 |
|
||||
| **Última tarea completada** | FASE 3 - Referencias y verificación COMPLETADO |
|
||||
| **Próxima tarea** | CASO PILOTO P.1 - Crear spec para AntiSpamValidator |
|
||||
| **Progreso total** | 66/75 tareas |
|
||||
|
||||
### Archivos de contexto a leer si es necesario:
|
||||
- `_planificacion/analisis-spam-formularios.md` - Análisis de spam
|
||||
- `_planificacion/validacion-specs-vs-codigo-actual.md` - Validación código vs specs
|
||||
- `_openspec/specs/` - Specs base del proyecto (3 archivos)
|
||||
- `_openspec/changes/` - Specs de features específicos
|
||||
- `D:\_Desarrollo\100Aditec\_openspec\` - Referencia de Aditec
|
||||
|
||||
---
|
||||
|
||||
## CHECKLIST MAESTRO DE PROGRESO
|
||||
|
||||
### Estado General
|
||||
- [x] Análisis de especificaciones Aditec completado
|
||||
- [x] Comparación ROI Theme vs Aditec completado
|
||||
- [x] Validación código actual vs especificaciones propuestas
|
||||
- [x] **FASE 1: Crear archivos faltantes** (COMPLETADO)
|
||||
- [x] **FASE 2: Expandir archivos existentes** (COMPLETADO)
|
||||
- [x] **FASE 3: Actualizar referencias** (COMPLETADO)
|
||||
- [ ] **CASO PILOTO: Implementar anti-spam con nuevo sistema**
|
||||
|
||||
---
|
||||
|
||||
## FASE 1: CREAR ARCHIVOS FALTANTES
|
||||
|
||||
### 1.1 WORKFLOW-ROI-THEME.md (~300 líneas)
|
||||
**Ubicación:** `_openspec/WORKFLOW-ROI-THEME.md`
|
||||
**Estado:** [x] NO INICIADO | [x] EN PROGRESO | [x] COMPLETADO
|
||||
|
||||
#### Secciones a crear:
|
||||
- [x] **1.1.1** Encabezado y metadata (version, fecha)
|
||||
- [x] **1.1.2** Regla de Oro del proyecto
|
||||
- [x] "SI NO EXISTE spec.md → NO SE TOCA CÓDIGO"
|
||||
- [x] Explicación de por qué esta regla
|
||||
- [x] **1.1.3** Flujo de 3 fases obligatorio
|
||||
- [x] Fase 1: Proponer (qué archivos se crean)
|
||||
- [x] Fase 2: Especificar (qué archivos se crean)
|
||||
- [x] Fase 3: Implementar (qué archivos se crean)
|
||||
- [x] **1.1.4** Flujo de 5 fases para componentes ROI Theme
|
||||
- [x] Fase 1: Schema JSON (ubicación, formato, campos obligatorios)
|
||||
- [x] Fase 2: Sincronización BD (comando WP-CLI)
|
||||
- [x] Fase 3: Renderer (ubicación, patrones, DI)
|
||||
- [x] Fase 4: FormBuilder (ubicación, patrones, Design System)
|
||||
- [x] Fase 5: Validación (script, qué valida)
|
||||
- [x] **1.1.5** Agentes disponibles y cuándo usarlos
|
||||
- [x] roi-schema-architect
|
||||
- [x] roi-renderer-builder
|
||||
- [x] roi-form-builder
|
||||
- [x] **1.1.6** Comandos WP-CLI disponibles
|
||||
- [x] sync-component
|
||||
- [x] sync-all-components
|
||||
- [x] Ejemplos de uso
|
||||
- [x] **1.1.7** Reglas de Clean Architecture resumidas
|
||||
- [x] Qué puede y no puede hacer Domain
|
||||
- [x] Qué puede y no puede hacer Application
|
||||
- [x] Qué puede y no puede hacer Infrastructure
|
||||
- [x] **1.1.8** Sección de Lecciones Aprendidas
|
||||
- [x] Errores comunes y cómo evitarlos
|
||||
- [x] Patrones que funcionan
|
||||
|
||||
---
|
||||
|
||||
### 1.2 nomenclatura.md (~600 líneas)
|
||||
**Ubicación:** `_openspec/specs/nomenclatura.md`
|
||||
**Estado:** [x] NO INICIADO | [x] EN PROGRESO | [x] COMPLETADO
|
||||
|
||||
#### Pre-requisitos:
|
||||
- [x] **1.2.0** Archivo plano en `_openspec/specs/` (sin carpeta anidada)
|
||||
|
||||
#### Secciones a crear:
|
||||
- [x] **1.2.1** Encabezado y Feature principal
|
||||
- [x] **1.2.2** Tabla resumen de TODAS las nomenclaturas (vista rápida)
|
||||
- [x] **1.2.3** Nomenclatura de Carpetas
|
||||
- [x] Carpetas principales (Admin/, Public/, Shared/)
|
||||
- [x] Carpetas de módulo (PascalCase)
|
||||
- [x] Carpetas auxiliares (_planificacion, _arquitectura)
|
||||
- [x] Carpetas de capas (Domain/, Application/, Infrastructure/)
|
||||
- [x] Ejemplos correctos e incorrectos
|
||||
- [x] **1.2.4** Nomenclatura de Archivos PHP
|
||||
- [x] Archivos de clase (PascalCase.php)
|
||||
- [x] Archivos de interface (PascalCaseInterface.php)
|
||||
- [x] Archivos de trait (PascalCaseTrait.php)
|
||||
- [x] Ejemplos correctos e incorrectos
|
||||
- [x] **1.2.5** Nomenclatura de Archivos JSON (Schemas)
|
||||
- [x] Nombres en kebab-case
|
||||
- [x] Extensión .json
|
||||
- [x] Ejemplos correctos e incorrectos
|
||||
- [x] **1.2.6** Nomenclatura de Namespaces
|
||||
- [x] Patrón: ROITheme\[Context]\[Component]\[Layer]
|
||||
- [x] Context: Admin, Public, Shared
|
||||
- [x] Layer: Domain, Application, Infrastructure
|
||||
- [x] Subcapas: Ui, Api, Persistence, Services
|
||||
- [x] Ejemplos completos
|
||||
- [x] **1.2.7** Nomenclatura de Clases
|
||||
- [x] Clases regulares (PascalCase)
|
||||
- [x] Renderers ([Component]Renderer)
|
||||
- [x] FormBuilders ([Component]FormBuilder)
|
||||
- [x] UseCases ([Action][Entity]UseCase)
|
||||
- [x] Services ([Entity]Service)
|
||||
- [x] Repositories ([Entity]Repository)
|
||||
- [x] Handlers ([Action]Handler)
|
||||
- [x] Ejemplos correctos e incorrectos
|
||||
- [x] **1.2.8** Nomenclatura de Interfaces
|
||||
- [x] Sufijo Interface o Contract
|
||||
- [x] Ubicación en Domain/Contracts/
|
||||
- [x] Ejemplos correctos e incorrectos
|
||||
- [x] **1.2.9** Nomenclatura de Métodos
|
||||
- [x] Métodos públicos (camelCase)
|
||||
- [x] Métodos privados (camelCase)
|
||||
- [x] Métodos booleanos (is*, has*, can*, should*)
|
||||
- [x] Getters/Setters (get*, set*)
|
||||
- [x] Ejemplos correctos e incorrectos
|
||||
- [x] **1.2.10** Nomenclatura de Propiedades
|
||||
- [x] Propiedades (camelCase)
|
||||
- [x] Propiedades booleanas (is*, has*)
|
||||
- [x] Visibilidad (private por defecto)
|
||||
- [x] Ejemplos correctos e incorrectos
|
||||
- [x] **1.2.11** Nomenclatura de Variables
|
||||
- [x] Variables locales ($camelCase)
|
||||
- [x] Parámetros ($camelCase)
|
||||
- [x] Variables de iteración ($item, $key, $i)
|
||||
- [x] Ejemplos correctos e incorrectos
|
||||
- [x] **1.2.12** Nomenclatura de Constantes
|
||||
- [x] Constantes de clase (UPPER_SNAKE_CASE)
|
||||
- [x] Constantes globales (ROI_THEME_*)
|
||||
- [x] Ejemplos correctos e incorrectos
|
||||
- [x] **1.2.13** Nomenclatura de component_name
|
||||
- [x] Formato kebab-case en JSON
|
||||
- [x] Formato kebab-case en BD
|
||||
- [x] Conversión kebab-case ↔ PascalCase
|
||||
- [x] Ejemplos de conversión
|
||||
- [x] **1.2.14** Nomenclatura de Hooks WordPress
|
||||
- [x] Actions (roi_theme_*)
|
||||
- [x] Filters (roi_theme_filter_*)
|
||||
- [x] Ejemplos correctos e incorrectos
|
||||
- [x] **1.2.15** Validación Pre-commit
|
||||
- [x] Checklist de nomenclatura
|
||||
- [x] Errores comunes a evitar
|
||||
|
||||
---
|
||||
|
||||
## FASE 2: EXPANDIR ARCHIVOS EXISTENTES
|
||||
|
||||
### 2.1 arquitectura-limpia.md (+485 líneas)
|
||||
**Ubicación:** `_openspec/specs/arquitectura-limpia.md`
|
||||
**Estado:** [x] NO INICIADO | [x] EN PROGRESO | [x] COMPLETADO
|
||||
|
||||
#### Secciones a agregar:
|
||||
- [x] **2.1.1** Diagrama ASCII de las 3 capas
|
||||
- [x] Domain (centro, sin dependencias)
|
||||
- [x] Application (usa Domain)
|
||||
- [x] Infrastructure (usa Application y Domain)
|
||||
- [x] Flechas de dependencia
|
||||
- [x] **2.1.2** Estructura completa de carpetas del tema
|
||||
- [x] Árbol completo roi-theme/
|
||||
- [x] Explicación de cada carpeta
|
||||
- [x] Cuándo crear nuevas carpetas
|
||||
- [x] **2.1.3** Reglas de anidamiento
|
||||
- [x] Profundidad máxima: 4 niveles
|
||||
- [x] Regla de 3 archivos para subcarpetas
|
||||
- [x] Ejemplos de estructuras válidas e inválidas
|
||||
- [x] **2.1.4** Diferencia entre niveles de Shared
|
||||
- [x] Shared/ (raíz) - código global
|
||||
- [x] Admin/Shared/ - compartido solo en Admin
|
||||
- [x] Public/Shared/ - compartido solo en Public
|
||||
- [x] Cuándo usar cada uno
|
||||
- [x] **2.1.5** Ejemplos de código PHP por capa
|
||||
- [x] Domain: Entidad correcta vs incorrecta
|
||||
- [x] Domain: Interface correcta vs incorrecta
|
||||
- [x] Application: UseCase correcto vs incorrecto
|
||||
- [x] Infrastructure: Repository correcto vs incorrecto
|
||||
- [x] Infrastructure: Renderer correcto vs incorrecto
|
||||
- [x] **2.1.6** Patrones de herencia
|
||||
- [x] Herencia para Renderers (si aplica)
|
||||
- [x] Herencia para FormBuilders (si aplica)
|
||||
- [x] Composición vs Herencia
|
||||
- [x] Profundidad máxima de herencia
|
||||
- [x] **2.1.7** Mapeo de terminología
|
||||
- [x] Tabla: Clean Architecture estándar → ROI Theme
|
||||
- [x] Entity → Component/Entity
|
||||
- [x] UseCase → UseCase
|
||||
- [x] Gateway → Repository
|
||||
- [x] Presenter → Renderer
|
||||
- [x] **2.1.8** Validación de arquitectura
|
||||
- [x] Script validate-architecture.php
|
||||
- [x] Qué valida
|
||||
- [x] Cómo ejecutarlo
|
||||
- [x] Cómo interpretar errores
|
||||
|
||||
---
|
||||
|
||||
### 2.2 estandares-codigo.md (+877 líneas)
|
||||
**Ubicación:** `_openspec/specs/estandares-codigo.md`
|
||||
**Estado:** [x] NO INICIADO | [x] EN PROGRESO | [x] COMPLETADO
|
||||
|
||||
#### Secciones a agregar:
|
||||
- [x] **2.2.1** Ejemplos de código PHP para SOLID
|
||||
- [x] SRP: Ejemplo correcto vs incorrecto
|
||||
- [x] OCP: Ejemplo correcto vs incorrecto
|
||||
- [x] LSP: Ejemplo correcto vs incorrecto
|
||||
- [x] ISP: Ejemplo correcto vs incorrecto
|
||||
- [x] DIP: Ejemplo correcto vs incorrecto
|
||||
- [x] **2.2.2** Manejo de errores WordPress
|
||||
- [x] Cuándo usar wp_die()
|
||||
- [x] Cuándo usar WP_Error
|
||||
- [x] Cuándo usar excepciones
|
||||
- [x] Ejemplos de cada caso
|
||||
- [x] **2.2.3** Sanitización y validación
|
||||
- [x] sanitize_text_field()
|
||||
- [x] sanitize_email()
|
||||
- [x] absint()
|
||||
- [x] wp_kses()
|
||||
- [x] Tabla de funciones por tipo de dato
|
||||
- [x] **2.2.4** Escaping para output
|
||||
- [x] esc_html()
|
||||
- [x] esc_attr()
|
||||
- [x] esc_url()
|
||||
- [x] esc_textarea()
|
||||
- [x] wp_kses_post()
|
||||
- [x] Tabla de funciones por contexto
|
||||
- [x] **2.2.5** Hooks WordPress
|
||||
- [x] add_action() - cuándo y cómo
|
||||
- [x] add_filter() - cuándo y cómo
|
||||
- [x] Prioridades
|
||||
- [x] Número de argumentos
|
||||
- [x] Ejemplos correctos
|
||||
- [x] **2.2.6** Recursos y cleanup
|
||||
- [x] Transients (set, get, delete)
|
||||
- [x] Object cache
|
||||
- [x] Cuándo limpiar recursos
|
||||
- [x] Ejemplos
|
||||
- [x] **2.2.7** Checklist pre-commit detallado
|
||||
- [x] Verificaciones de sintaxis
|
||||
- [x] Verificaciones de estilo
|
||||
- [x] Verificaciones de seguridad
|
||||
- [x] Verificaciones de arquitectura
|
||||
- [x] Verificaciones de nomenclatura
|
||||
|
||||
---
|
||||
|
||||
## FASE 3: ACTUALIZAR REFERENCIAS
|
||||
|
||||
### 3.1 Actualizar project.md
|
||||
**Ubicación:** `_openspec/project.md`
|
||||
**Estado:** [x] NO INICIADO | [x] EN PROGRESO | [x] COMPLETADO
|
||||
|
||||
- [x] **3.1.1** Agregar referencia a WORKFLOW-ROI-THEME.md
|
||||
- [x] **3.1.2** Agregar referencia a nomenclatura/spec.md
|
||||
- [x] **3.1.3** Actualizar índice de specs
|
||||
- [x] **3.1.4** Verificar que links funcionan
|
||||
|
||||
### 3.2 Cross-references entre specs
|
||||
**Estado:** [x] NO INICIADO | [x] EN PROGRESO | [x] COMPLETADO
|
||||
|
||||
- [x] **3.2.1** arquitectura-limpia → nomenclatura (referencias a nombres)
|
||||
- [x] **3.2.2** estandares-codigo → arquitectura-limpia (referencias a capas)
|
||||
- [x] **3.2.3** nomenclatura → estandares-codigo (referencias a convenciones)
|
||||
- [x] **3.2.4** WORKFLOW → todas las specs (referencias a cada fase)
|
||||
|
||||
### 3.3 Verificación final
|
||||
**Estado:** [x] NO INICIADO | [x] EN PROGRESO | [x] COMPLETADO
|
||||
|
||||
- [x] **3.3.1** Consistencia de ejemplos entre archivos
|
||||
- [x] **3.3.2** Todos los links funcionan
|
||||
- [x] **3.3.3** No hay contradicciones entre specs
|
||||
- [x] **3.3.4** Todos los archivos tienen fecha de actualización
|
||||
|
||||
---
|
||||
|
||||
## CASO PILOTO: ANTI-SPAM
|
||||
|
||||
### Implementación usando nuevo sistema de specs
|
||||
**Estado:** [ ] NO INICIADO | [ ] EN PROGRESO | [ ] COMPLETADO
|
||||
|
||||
- [ ] **P.1** Crear spec para AntiSpamValidator siguiendo WORKFLOW
|
||||
- [ ] **P.2** Validar que spec cumple nomenclatura
|
||||
- [ ] **P.3** Validar que spec cumple arquitectura-limpia
|
||||
- [ ] **P.4** Validar que spec cumple estandares-codigo
|
||||
- [ ] **P.5** Implementar AntiSpamValidator
|
||||
- [ ] **P.6** Aplicar a Newsletter Footer
|
||||
- [ ] **P.7** Aplicar a Contact Form
|
||||
- [ ] **P.8** Probar que spam es rechazado
|
||||
- [ ] **P.9** Documentar lecciones aprendidas en WORKFLOW
|
||||
|
||||
---
|
||||
|
||||
## RESUMEN DE ARCHIVOS A CREAR/MODIFICAR
|
||||
|
||||
| # | Archivo | Acción | Líneas | Estado |
|
||||
|---|---------|--------|--------|--------|
|
||||
| 1 | `_openspec/WORKFLOW-ROI-THEME.md` | CREAR | ~438 | [x] COMPLETADO |
|
||||
| 2 | `_openspec/specs/nomenclatura.md` | CREAR | ~687 | [x] COMPLETADO |
|
||||
| 3 | `_openspec/specs/arquitectura-limpia.md` | EXPANDIR | ~770 | [x] COMPLETADO |
|
||||
| 4 | `_openspec/specs/estandares-codigo.md` | EXPANDIR | ~1228 | [x] COMPLETADO |
|
||||
| 5 | `_openspec/project.md` | ACTUALIZAR | ~107 | [x] COMPLETADO |
|
||||
|
||||
### Estructura Final
|
||||
```
|
||||
_openspec/
|
||||
├── AGENTS.md
|
||||
├── WORKFLOW-ROI-THEME.md
|
||||
├── project.md
|
||||
├── specs/ # Solo specs BASE
|
||||
│ ├── arquitectura-limpia.md
|
||||
│ ├── estandares-codigo.md
|
||||
│ └── nomenclatura.md
|
||||
└── changes/ # Features/cambios específicos
|
||||
├── add-advanced-incontent-ads/
|
||||
├── adsense-auto-ads-toggle/
|
||||
├── adsense-cache-unified-visibility/
|
||||
├── adsense-javascript-first/
|
||||
├── cache-first-architecture/
|
||||
├── flujo-componentes/
|
||||
├── patrones-wordpress/
|
||||
├── post-grid-shortcode/
|
||||
├── refactor-adsense-lazy-loading/
|
||||
└── templates-unificados/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## MÉTRICAS DE PROGRESO
|
||||
|
||||
### Contadores actuales:
|
||||
- **Tareas FASE 1:** 32/32 completadas
|
||||
- **Tareas FASE 2:** 23/23 completadas
|
||||
- **Tareas FASE 3:** 11/11 completadas
|
||||
- **Tareas PILOTO:** 0/9 completadas
|
||||
- **TOTAL:** 66/75 tareas completadas
|
||||
|
||||
### Al completar:
|
||||
| Métrica | Antes | Después |
|
||||
|---------|-------|---------|
|
||||
| Total líneas specs | ~1021 | ~2200 |
|
||||
| Archivos fundamentales | 3/5 | 5/5 |
|
||||
| Diagramas ASCII | 0 | 3+ |
|
||||
| Ejemplos código | ~5 | ~30 |
|
||||
| Checklists | 1 parcial | 3 completos |
|
||||
|
||||
---
|
||||
|
||||
**Fin del documento**
|
||||
1121
_planificacion/roi-theme-template/css/style.css
Normal file
1121
_planificacion/roi-theme-template/css/style.css
Normal file
File diff suppressed because it is too large
Load Diff
BIN
_planificacion/roi-theme-template/img/featured-image.png
Normal file
BIN
_planificacion/roi-theme-template/img/featured-image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 256 KiB |
1159
_planificacion/roi-theme-template/index.html
Normal file
1159
_planificacion/roi-theme-template/index.html
Normal file
File diff suppressed because it is too large
Load Diff
317
_planificacion/roi-theme-template/js/main.js
Normal file
317
_planificacion/roi-theme-template/js/main.js
Normal file
@@ -0,0 +1,317 @@
|
||||
/**
|
||||
* APU MÉXICO - MAIN JAVASCRIPT
|
||||
*/
|
||||
|
||||
// Navbar scroll effect
|
||||
window.addEventListener('scroll', function() {
|
||||
const navbar = document.querySelector('.navbar');
|
||||
if (navbar) {
|
||||
if (window.scrollY > 50) {
|
||||
navbar.classList.add('scrolled');
|
||||
} else {
|
||||
navbar.classList.remove('scrolled');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Table of Contents - ScrollSpy
|
||||
function updateActiveSection() {
|
||||
const tocLinks = document.querySelectorAll('.toc-container a');
|
||||
if (!tocLinks.length) return;
|
||||
|
||||
const navbar = document.querySelector('.navbar');
|
||||
const navbarHeight = navbar ? navbar.offsetHeight : 0;
|
||||
|
||||
const sectionIds = Array.from(tocLinks).map(link => {
|
||||
const href = link.getAttribute('href');
|
||||
return href ? href.substring(1) : null;
|
||||
}).filter(id => id !== null);
|
||||
|
||||
const sections = sectionIds.map(id => document.getElementById(id)).filter(el => el !== null);
|
||||
const scrollPosition = window.scrollY + navbarHeight + 100;
|
||||
|
||||
let activeSection = null;
|
||||
|
||||
for (let i = 0; i < sections.length; i++) {
|
||||
const section = sections[i];
|
||||
const sectionTop = section.offsetTop;
|
||||
|
||||
if (scrollPosition >= sectionTop) {
|
||||
activeSection = section.getAttribute('id');
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
tocLinks.forEach(link => {
|
||||
link.classList.remove('active');
|
||||
const href = link.getAttribute('href');
|
||||
if (href === '#' + activeSection) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Smooth scroll for TOC links
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.querySelectorAll('.toc-container a').forEach(anchor => {
|
||||
anchor.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
const targetId = this.getAttribute('href');
|
||||
const targetElement = document.querySelector(targetId);
|
||||
|
||||
if (targetElement) {
|
||||
const navbar = document.querySelector('.navbar');
|
||||
const navbarHeight = navbar ? navbar.offsetHeight : 0;
|
||||
const offsetTop = targetElement.offsetTop - navbarHeight - 40;
|
||||
|
||||
window.scrollTo({
|
||||
top: offsetTop,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
updateActiveSection();
|
||||
});
|
||||
|
||||
window.addEventListener('scroll', updateActiveSection);
|
||||
|
||||
// A/B Testing for CTA sections
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const ctaVariant = Math.random() < 0.5 ? 'A' : 'B';
|
||||
|
||||
if (ctaVariant === 'A') {
|
||||
const variantA = document.querySelector('.cta-variant-a');
|
||||
if (variantA) variantA.style.display = 'block';
|
||||
} else {
|
||||
const variantB = document.querySelector('.cta-variant-b');
|
||||
if (variantB) variantB.style.display = 'block';
|
||||
}
|
||||
|
||||
document.querySelectorAll('.cta-button').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const variant = this.getAttribute('data-cta-variant');
|
||||
console.log('CTA clicked - Variant: ' + variant);
|
||||
|
||||
if (typeof gtag !== 'undefined') {
|
||||
gtag('event', 'cta_click', {
|
||||
'event_category': 'CTA',
|
||||
'event_label': 'Variant_' + variant,
|
||||
'value': variant
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Contact Modal - Dynamic Loading
|
||||
function loadContactModal() {
|
||||
const modalContainer = document.getElementById('modalContainer');
|
||||
if (!modalContainer) return;
|
||||
|
||||
fetch('modal-contact.html')
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
modalContainer.innerHTML = html;
|
||||
initContactForm();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading modal:', error);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', loadContactModal);
|
||||
|
||||
// Contact Form - Webhook Submission
|
||||
function initContactForm() {
|
||||
const form = document.getElementById('contactForm');
|
||||
if (!form) return;
|
||||
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const WEBHOOK_URL = 'https://tu-webhook.com/contacto';
|
||||
|
||||
const formData = {
|
||||
fullName: document.getElementById('fullName').value,
|
||||
company: document.getElementById('company').value,
|
||||
whatsapp: document.getElementById('whatsapp').value,
|
||||
email: document.getElementById('email').value,
|
||||
comments: document.getElementById('comments').value,
|
||||
timestamp: new Date().toISOString(),
|
||||
source: 'APU Website - Modal'
|
||||
};
|
||||
|
||||
if (!formData.fullName || !formData.whatsapp || !formData.email) {
|
||||
showFormMessage('Por favor completa todos los campos requeridos', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(formData.email)) {
|
||||
showFormMessage('Por favor ingresa un correo electrónico válido', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
const submitButton = form.querySelector('button[type="submit"]');
|
||||
const originalText = submitButton.innerHTML;
|
||||
submitButton.disabled = true;
|
||||
submitButton.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Enviando...';
|
||||
|
||||
fetch(WEBHOOK_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(formData)
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error('Error en el envío');
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
showFormMessage('¡Mensaje enviado exitosamente!', 'success');
|
||||
form.reset();
|
||||
|
||||
if (typeof gtag !== 'undefined') {
|
||||
gtag('event', 'form_submission', {
|
||||
'event_category': 'Contact Form',
|
||||
'event_label': 'Form Submitted'
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('contactModal'));
|
||||
if (modal) modal.hide();
|
||||
}, 2000);
|
||||
})
|
||||
.catch(error => {
|
||||
showFormMessage('Error al enviar el mensaje', 'danger');
|
||||
})
|
||||
.finally(() => {
|
||||
submitButton.disabled = false;
|
||||
submitButton.innerHTML = originalText;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function showFormMessage(message, type) {
|
||||
const messageDiv = document.getElementById('formMessage');
|
||||
if (!messageDiv) return;
|
||||
|
||||
messageDiv.textContent = message;
|
||||
messageDiv.className = `mt-3 alert alert-${type}`;
|
||||
messageDiv.style.display = 'block';
|
||||
|
||||
setTimeout(() => {
|
||||
messageDiv.style.display = 'none';
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Footer Contact Form
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const footerForm = document.getElementById('footerContactForm');
|
||||
if (!footerForm) return;
|
||||
|
||||
footerForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const WEBHOOK_URL = 'https://tu-webhook.com/contacto';
|
||||
|
||||
const formData = {
|
||||
fullName: document.getElementById('footerFullName').value,
|
||||
company: document.getElementById('footerCompany').value,
|
||||
whatsapp: document.getElementById('footerWhatsapp').value,
|
||||
email: document.getElementById('footerEmail').value,
|
||||
comments: document.getElementById('footerComments').value,
|
||||
timestamp: new Date().toISOString(),
|
||||
source: 'APU Website - Footer'
|
||||
};
|
||||
|
||||
if (!formData.fullName || !formData.whatsapp || !formData.email) {
|
||||
showFooterFormMessage('Por favor completa todos los campos requeridos', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(formData.email)) {
|
||||
showFooterFormMessage('Por favor ingresa un correo válido', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
const submitButton = footerForm.querySelector('button[type="submit"]');
|
||||
const originalText = submitButton.innerHTML;
|
||||
submitButton.disabled = true;
|
||||
submitButton.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Enviando...';
|
||||
|
||||
fetch(WEBHOOK_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(formData)
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error('Error en el envío');
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
showFooterFormMessage('¡Mensaje enviado exitosamente!', 'success');
|
||||
footerForm.reset();
|
||||
|
||||
if (typeof gtag !== 'undefined') {
|
||||
gtag('event', 'form_submission', {
|
||||
'event_category': 'Footer Form',
|
||||
'event_label': 'Form Submitted'
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showFooterFormMessage('Error al enviar el mensaje', 'danger');
|
||||
})
|
||||
.finally(() => {
|
||||
submitButton.disabled = false;
|
||||
submitButton.innerHTML = originalText;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function showFooterFormMessage(message, type) {
|
||||
const messageDiv = document.getElementById('footerFormMessage');
|
||||
if (!messageDiv) return;
|
||||
|
||||
messageDiv.textContent = message;
|
||||
messageDiv.className = `col-12 mt-2 alert alert-${type}`;
|
||||
messageDiv.style.display = 'block';
|
||||
|
||||
setTimeout(() => {
|
||||
messageDiv.style.display = 'none';
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Smooth scroll for all anchor links
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function (e) {
|
||||
const href = this.getAttribute('href');
|
||||
|
||||
if (href === '#' || this.getAttribute('data-bs-toggle') === 'modal') {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetElement = document.querySelector(href);
|
||||
if (!targetElement) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const navbar = document.querySelector('.navbar');
|
||||
const navbarHeight = navbar ? navbar.offsetHeight : 0;
|
||||
const offsetTop = targetElement.offsetTop - navbarHeight - 20;
|
||||
|
||||
window.scrollTo({
|
||||
top: offsetTop,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
console.log('%c APU México ', 'background: #1e3a5f; color: #FF8600; font-size: 16px; font-weight: bold; padding: 10px;');
|
||||
42
_planificacion/roi-theme-template/modal-contact.html
Normal file
42
_planificacion/roi-theme-template/modal-contact.html
Normal file
@@ -0,0 +1,42 @@
|
||||
<!-- Contact Modal -->
|
||||
<div class="modal fade" id="contactModal" tabindex="-1" aria-labelledby="contactModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header border-0">
|
||||
<h5 class="modal-title fw-bold" id="contactModalLabel">¿Listo para comenzar?</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body px-4 pb-4">
|
||||
<p class="text-muted mb-4">Completa el formulario y nos pondremos en contacto contigo lo antes posible.</p>
|
||||
<form id="contactForm">
|
||||
<div class="mb-3">
|
||||
<label for="fullName" class="form-label">Nombre completo <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="fullName" name="fullName" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="company" class="form-label">Empresa</label>
|
||||
<input type="text" class="form-control" id="company" name="company">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="whatsapp" class="form-label">WhatsApp <span class="text-danger">*</span></label>
|
||||
<input type="tel" class="form-control" id="whatsapp" name="whatsapp" placeholder="+52 ___ ___ ____" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Correo electrónico <span class="text-danger">*</span></label>
|
||||
<input type="email" class="form-control" id="email" name="email" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="comments" class="form-label">Comentarios</label>
|
||||
<textarea class="form-control" id="comments" name="comments" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-submit-form btn-lg">
|
||||
Enviar
|
||||
</button>
|
||||
</div>
|
||||
<div id="formMessage" class="mt-3 alert" style="display: none;"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
335
_planificacion/validacion-specs-vs-codigo-actual.md
Normal file
335
_planificacion/validacion-specs-vs-codigo-actual.md
Normal file
@@ -0,0 +1,335 @@
|
||||
# Validación: Especificaciones vs Código Actual
|
||||
|
||||
**Fecha:** 2026-01-08
|
||||
**Objetivo:** Verificar que las especificaciones reflejan el código existente
|
||||
|
||||
---
|
||||
|
||||
## Resumen Ejecutivo
|
||||
|
||||
**CONCLUSIÓN: El código actual YA cumple con las especificaciones propuestas.**
|
||||
|
||||
Las mejoras a las especificaciones son documentación/clarificación de patrones existentes, NO nuevas invenciones.
|
||||
|
||||
---
|
||||
|
||||
## 1. Arquitectura Clean Architecture
|
||||
|
||||
### Verificación de Estructura de Capas
|
||||
|
||||
| Capa | Ubicación Esperada | Estado |
|
||||
|------|-------------------|--------|
|
||||
| Domain | `*/Domain/` | ✅ Existe |
|
||||
| Application | `*/Application/` | ✅ Existe |
|
||||
| Infrastructure | `*/Infrastructure/` | ✅ Existe |
|
||||
|
||||
### Evidencia: Estructura de Carpetas
|
||||
|
||||
```
|
||||
Shared/
|
||||
├── Domain/
|
||||
│ └── Contracts/ # 23 interfaces
|
||||
├── Application/
|
||||
│ └── UseCases/ # Casos de uso
|
||||
└── Infrastructure/
|
||||
└── Services/ # Implementaciones
|
||||
```
|
||||
|
||||
### Evidencia: Contratos en Domain (23 interfaces)
|
||||
|
||||
```
|
||||
Shared/Domain/Contracts/
|
||||
├── AjaxControllerInterface.php
|
||||
├── CSSGeneratorInterface.php
|
||||
├── ComponentRepositoryInterface.php
|
||||
├── RendererInterface.php
|
||||
├── SchemaSyncServiceInterface.php
|
||||
└── ... (18 más)
|
||||
```
|
||||
|
||||
**VEREDICTO:** ✅ Clean Architecture implementada correctamente
|
||||
|
||||
---
|
||||
|
||||
## 2. Nomenclatura
|
||||
|
||||
### Convención: Carpetas en PascalCase
|
||||
|
||||
| Módulo | Carpeta Public | Carpeta Admin | Estado |
|
||||
|--------|---------------|---------------|--------|
|
||||
| ContactForm | `Public/ContactForm/` | `Admin/ContactForm/` | ✅ |
|
||||
| FeaturedImage | `Public/FeaturedImage/` | `Admin/FeaturedImage/` | ✅ |
|
||||
| Footer | `Public/Footer/` | `Admin/Footer/` | ✅ |
|
||||
| TopNotificationBar | `Public/TopNotificationBar/` | `Admin/TopNotificationBar/` | ✅ |
|
||||
|
||||
**Módulos verificados:** 17 en Public/, 17 en Admin/
|
||||
|
||||
### Convención: Schemas en kebab-case
|
||||
|
||||
| Schema | Nombre Archivo | component_name | Estado |
|
||||
|--------|---------------|----------------|--------|
|
||||
| Contact Form | `contact-form.json` | `"contact-form"` | ✅ |
|
||||
| Featured Image | `featured-image.json` | `"featured-image"` | ✅ |
|
||||
| Top Notification Bar | `top-notification-bar.json` | `"top-notification-bar"` | ✅ |
|
||||
| CTA Box Sidebar | `cta-box-sidebar.json` | `"cta-box-sidebar"` | ✅ |
|
||||
|
||||
**Schemas verificados:** 17 archivos JSON
|
||||
|
||||
### Convención: Clases en PascalCase
|
||||
|
||||
| Tipo | Patrón | Ejemplo Real | Estado |
|
||||
|------|--------|--------------|--------|
|
||||
| Renderer | `[Component]Renderer` | `ContactFormRenderer` | ✅ |
|
||||
| FormBuilder | `[Component]FormBuilder` | `ContactFormFormBuilder` | ✅ |
|
||||
| Handler | `[Component]AjaxHandler` | `NewsletterAjaxHandler` | ✅ |
|
||||
|
||||
### Convención: Namespaces
|
||||
|
||||
**Patrón:** `ROITheme\[Context]\[Component]\[Layer]`
|
||||
|
||||
**Evidencia:**
|
||||
```php
|
||||
// ContactFormRenderer.php
|
||||
namespace ROITheme\Public\ContactForm\Infrastructure\Ui;
|
||||
|
||||
// ContactFormFormBuilder.php
|
||||
namespace ROITheme\Admin\ContactForm\Infrastructure\Ui;
|
||||
|
||||
// NewsletterAjaxHandler.php
|
||||
namespace ROITheme\Public\Footer\Infrastructure\Api\WordPress;
|
||||
```
|
||||
|
||||
**VEREDICTO:** ✅ Nomenclatura consistente en todo el tema
|
||||
|
||||
---
|
||||
|
||||
## 3. Estándares de Código PHP
|
||||
|
||||
### strict_types
|
||||
|
||||
**Comando verificación:** `grep "declare(strict_types=1)" Public/ -r`
|
||||
**Resultado:** 43 archivos con strict_types
|
||||
|
||||
| Ubicación | Archivos con strict_types | Total |
|
||||
|-----------|--------------------------|-------|
|
||||
| Public/ | 43 | 43 |
|
||||
| Admin/ (muestra) | ✅ Verificado | - |
|
||||
| Shared/ (muestra) | ✅ Verificado | - |
|
||||
|
||||
### Clases final
|
||||
|
||||
**Resultado:** 39 archivos usan `final class`
|
||||
|
||||
**Evidencia:**
|
||||
```php
|
||||
// ContactFormRenderer.php:24
|
||||
final class ContactFormRenderer implements RendererInterface
|
||||
|
||||
// ContactFormFormBuilder.php:19
|
||||
final class ContactFormFormBuilder
|
||||
|
||||
// NewsletterAjaxHandler.php
|
||||
final class NewsletterAjaxHandler
|
||||
```
|
||||
|
||||
### Inyección de Dependencias via Constructor
|
||||
|
||||
**Patrón esperado:** Interfaces inyectadas, no clases concretas
|
||||
|
||||
**Evidencia:**
|
||||
```php
|
||||
// ContactFormRenderer.php:28-30
|
||||
public function __construct(
|
||||
private CSSGeneratorInterface $cssGenerator // ✅ Interface
|
||||
) {}
|
||||
|
||||
// ContactFormFormBuilder.php:21-23
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer // ✅ DI via constructor
|
||||
) {}
|
||||
```
|
||||
|
||||
**VEREDICTO:** ✅ Estándares PHP cumplidos
|
||||
|
||||
---
|
||||
|
||||
## 4. Patrones de Renderer
|
||||
|
||||
### Patrón: supports() retorna kebab-case
|
||||
|
||||
**Evidencia ContactFormRenderer.php:71-74:**
|
||||
```php
|
||||
private const COMPONENT_NAME = 'contact-form'; // ✅ kebab-case
|
||||
|
||||
public function supports(string $componentType): bool
|
||||
{
|
||||
return $componentType === self::COMPONENT_NAME; // ✅
|
||||
}
|
||||
```
|
||||
|
||||
### Patrón: Validación de Visibilidad (3 campos)
|
||||
|
||||
**Evidencia ContactFormRenderer.php:76-99:**
|
||||
```php
|
||||
private function isEnabled(array $data): bool
|
||||
{
|
||||
$value = $data['visibility']['is_enabled'] ?? false; // ✅
|
||||
return $value === true || $value === '1' || $value === 1;
|
||||
}
|
||||
|
||||
private function getVisibilityClass(array $data): ?string
|
||||
{
|
||||
$showDesktop = $data['visibility']['show_on_desktop'] ?? true; // ✅
|
||||
$showMobile = $data['visibility']['show_on_mobile'] ?? true; // ✅
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Patrón: CSS via Generator (no hardcodeado)
|
||||
|
||||
**Evidencia ContactFormRenderer.php:49:**
|
||||
```php
|
||||
$css = $this->generateCSS($data); // ✅ Usa CSSGenerator inyectado
|
||||
```
|
||||
|
||||
**VEREDICTO:** ✅ Patrones de Renderer cumplidos
|
||||
|
||||
---
|
||||
|
||||
## 5. Patrones de FormBuilder
|
||||
|
||||
### Patrón: Design System Consistente
|
||||
|
||||
**Colores esperados:** `#0E2337`, `#1e3a5f`, `#FF8600`
|
||||
|
||||
**Evidencia ContactFormFormBuilder.php:57-58:**
|
||||
```php
|
||||
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
```
|
||||
|
||||
### Patrón: data-component en kebab-case
|
||||
|
||||
**Evidencia ContactFormFormBuilder.php:69:**
|
||||
```php
|
||||
$html .= '... data-component="contact-form">'; // ✅ kebab-case
|
||||
```
|
||||
|
||||
### Patrón: Bootstrap 5
|
||||
|
||||
**Evidencia ContactFormFormBuilder.php:31:**
|
||||
```php
|
||||
$html .= '<div class="row g-3">'; // ✅ Bootstrap 5 grid
|
||||
```
|
||||
|
||||
**VEREDICTO:** ✅ Patrones de FormBuilder cumplidos
|
||||
|
||||
---
|
||||
|
||||
## 6. Patrones de Schema JSON
|
||||
|
||||
### Campos Obligatorios de Visibilidad
|
||||
|
||||
**Evidencia contact-form.json:6-31:**
|
||||
```json
|
||||
"visibility": {
|
||||
"label": "Visibilidad",
|
||||
"priority": 10,
|
||||
"fields": {
|
||||
"is_enabled": { ... }, // ✅ Obligatorio
|
||||
"show_on_desktop": { ... }, // ✅ Obligatorio
|
||||
"show_on_mobile": { ... } // ✅ Obligatorio
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Estructura de Grupos
|
||||
|
||||
**Evidencia contact-form.json:**
|
||||
```json
|
||||
{
|
||||
"component_name": "contact-form", // ✅ kebab-case
|
||||
"version": "1.0.0", // ✅ Versionado
|
||||
"description": "...", // ✅ Documentado
|
||||
"groups": {
|
||||
"visibility": { "priority": 10 },
|
||||
"content": { "priority": 20 },
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**VEREDICTO:** ✅ Patrones de Schema cumplidos
|
||||
|
||||
---
|
||||
|
||||
## 7. Estructura OpenSpec Actual
|
||||
|
||||
### Archivos Existentes
|
||||
|
||||
```
|
||||
openspec/
|
||||
├── AGENTS.md ✅ (456 líneas)
|
||||
├── project.md ✅ (contiene convenciones)
|
||||
├── specs/
|
||||
│ ├── 00arquitectura-limpia/spec.md ✅ (215 líneas)
|
||||
│ ├── 00estandares-codigo/spec.md ✅ (350 líneas)
|
||||
│ └── ...
|
||||
└── changes/ ✅ (registro de cambios)
|
||||
```
|
||||
|
||||
### Archivos Faltantes (para mejorar)
|
||||
|
||||
| Archivo | Estado | Acción |
|
||||
|---------|--------|--------|
|
||||
| WORKFLOW-ROI-THEME.md | ❌ No existe | CREAR |
|
||||
| nomenclatura/spec.md | ❌ No existe | CREAR |
|
||||
|
||||
**NOTA:** Los archivos faltantes son DOCUMENTACIÓN de patrones que YA existen en el código.
|
||||
|
||||
---
|
||||
|
||||
## 8. Tabla Resumen de Validación
|
||||
|
||||
| Especificación | Código Actual | Estado |
|
||||
|----------------|---------------|--------|
|
||||
| Clean Architecture (3 capas) | Shared/Domain, /Application, /Infrastructure | ✅ CUMPLE |
|
||||
| Carpetas PascalCase | ContactForm/, FeaturedImage/, etc. | ✅ CUMPLE |
|
||||
| Schemas kebab-case | contact-form.json, featured-image.json | ✅ CUMPLE |
|
||||
| component_name kebab-case | "contact-form", "featured-image" | ✅ CUMPLE |
|
||||
| strict_types=1 | 43+ archivos verificados | ✅ CUMPLE |
|
||||
| final class | 39+ clases verificadas | ✅ CUMPLE |
|
||||
| DI via constructor | CSSGeneratorInterface, etc. | ✅ CUMPLE |
|
||||
| supports() kebab-case | return 'contact-form' | ✅ CUMPLE |
|
||||
| 3 campos visibilidad | is_enabled, show_on_desktop, show_on_mobile | ✅ CUMPLE |
|
||||
| Design System colores | #0E2337, #1e3a5f, #FF8600 | ✅ CUMPLE |
|
||||
| Namespaces ROITheme\... | ROITheme\Public\ContactForm\... | ✅ CUMPLE |
|
||||
|
||||
---
|
||||
|
||||
## 9. Conclusión
|
||||
|
||||
### El código existente YA implementa:
|
||||
|
||||
1. **Clean Architecture** - Capas Domain/Application/Infrastructure
|
||||
2. **Convenciones de Nomenclatura** - PascalCase, kebab-case según contexto
|
||||
3. **Estándares PHP** - strict_types, final class, DI
|
||||
4. **Patrones de Componentes** - Renderer, FormBuilder, Schema
|
||||
5. **Design System** - Colores consistentes
|
||||
|
||||
### Las especificaciones mejoradas serán:
|
||||
|
||||
1. **DOCUMENTACIÓN** de patrones existentes (no invenciones)
|
||||
2. **CLARIFICACIÓN** de reglas implícitas
|
||||
3. **EJEMPLOS** de código correcto vs incorrecto
|
||||
4. **WORKFLOW** formalizado (que ya se sigue informalmente)
|
||||
|
||||
### Beneficio de las mejoras:
|
||||
|
||||
- Mayor claridad para nuevos desarrolladores
|
||||
- Referencia rápida de patrones
|
||||
- Validación automatizable
|
||||
- Onboarding más rápido
|
||||
|
||||
---
|
||||
|
||||
**Última actualización:** 2026-01-08
|
||||
Reference in New Issue
Block a user