- 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>
13 KiB
13 KiB
💾 PERSISTENCIA EN ARCHIVOS JSON
Estructura del Archivo config.json
Cada componente DEBE tener su archivo config.json en su carpeta:
{
"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
/**
* 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
/**
* 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
/**
* 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
/**
* 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:
/**
* 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
/**
* 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
/**
* 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
/**
* 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);
}