Saltar al contenido principal

Características que marcan la diferencia

Herramientas pensadas para maximizar la efectividad de tus ceremonias ágiles

groups

Colaboración en tiempo real

Participa simultáneamente con tu equipo, sin límites de salas ni participantes. Experimenta la verdadera colaboración distribuida.

analytics

Estimaciones inteligentes

Algoritmos que detectan consenso automáticamente y proporcionan métricas visuales para mejorar la precisión de tus estimaciones.

tune

Personalización total

Adapta temas, reglas y tipos de cartas a tu metodología específica. Flexibilidad sin comprometer la usabilidad.

import_export

Integración perfecta

Importa bancos de tareas y exporta resultados hacia Jira, Azure DevOps, ClickUp y otras plataformas de gestión.

psychology

Retrospectivas efectivas

Tableros colaborativos con votación, categorización automática y seguimiento de elementos de acción.

schedule

Gestión inteligente

Temporizadores para breaks, historial de sesiones y métricas de rendimiento para optimizar tus ceremonias.

Experimenta las herramientas

Prueba nuestras funcionalidades principales con demos completamente interactivas

casino

Planning Poker

Experimenta estimaciones colaborativas con cartas Fibonacci, detección de consenso y métricas en tiempo real.

forum

Retrospectivas

Tableros colaborativos para identificar qué funcionó bien, qué mejorar y definir acciones concretas.

Conecta con tu stack existente

Integración nativa con DoSoft To-Do y exportación estructurada hacia las principales plataformas de gestión

To-Do by DoSoft

DoSoft To-Do

Sincronización nativa y gestión completa

Jira

Jira

Exportación CSV/JSON estructurada

Azure DevOps

Azure DevOps

Compatible con Azure Boards

ClickUp

ClickUp

Importación directa de tareas

¿Listo para transformar tus ceremonias ágiles?

Únete a equipos que ya están mejorando su colaboración y obteniendo estimaciones más precisas.

launch Comenzar gratis
this.toggleTheme(e.target.checked); }); } // Mobile menu if (this.elements.mobileMenuBtn && this.elements.mobileNav) { this.elements.mobileMenuBtn.addEventListener('click', () => { this.elements.mobileNav.classList.toggle('active'); this.elements.mobileMenuBtn.classList.toggle('active'); this.elements.mobileMenuBtn.setAttribute('aria-expanded', this.elements.mobileNav.classList.contains('active')); }); } // TXT Import functionality if (this.elements.importTxtBtn) { this.elements.importTxtBtn.addEventListener('click', () => { this.elements.txtFileInput.click(); }); } if (this.elements.txtFileInput) { this.elements.txtFileInput.addEventListener('change', (e) => { this.handleTxtFileUpload(e); }); } if (this.elements.confirmImport) { this.elements.confirmImport.addEventListener('click', () => { this.confirmTaskImport(); }); } if (this.elements.cancelImport) { this.elements.cancelImport.addEventListener('click', () => { this.cancelTaskImport(); }); } // Demo Backlog button if (this.elements.demoBacklogBtn) { this.elements.demoBacklogBtn.addEventListener('click', () => { this.openDemoOverlay('backlog'); }); } // Demo overlay controls if (this.elements.demoCloseBtn) { this.elements.demoCloseBtn.addEventListener('click', () => { this.closeDemoOverlay(); }); } if (this.elements.demoResetBtn) { this.elements.demoResetBtn.addEventListener('click', () => { this.resetCurrentDemo(); }); } // Tab switching this.elements.tabButtons.forEach(tab => { tab.addEventListener('click', () => { this.switchDemoTab(tab.id.replace('tab', '').toLowerCase()); }); }); // Close overlay when clicking backdrop if (this.elements.demoOverlay) { this.elements.demoOverlay.addEventListener('click', (e) => { if (e.target.classList.contains('demo-overlay__backdrop')) { this.closeDemoOverlay(); } }); } // Scroll effects this.initializeScrollEffects(); // Auto-save on state changes if (this.state.settings.autoSave) { setInterval(() => this.saveState(), 30000); // Save every 30 seconds } // TXT Import Functionality initializeTxtImport() { this.pendingTasks = []; } handleTxtFileUpload(event) { const file = event.target.files[0]; if (!file) return; if (!file.name.endsWith('.txt')) { this.showImportStatus('Solo se aceptan archivos .txt', 'error'); return; } const reader = new FileReader(); reader.onload = (e) => { this.processTxtContent(e.target.result); }; reader.onerror = () => { this.showImportStatus('Error al leer el archivo', 'error'); }; reader.readAsText(file); } processTxtContent(content) { const lines = content.split('\n').filter(line => line.trim()); this.pendingTasks = []; const taskRegex = /task\s+(\d+)\s+(.+?):\s*(.+),?$/i; lines.forEach((line, index) => { const match = line.trim().match(taskRegex); if (match) { const [, id, title, description] = match; const task = { id: parseInt(id), title: title.trim(), description: description.trim(), type: this.classifyTask(title, description), status: 'backlog', points: null, created: new Date().toISOString() }; this.pendingTasks.push(task); } }); if (this.pendingTasks.length === 0) { this.showImportStatus('No se encontraron tareas válidas en el formato esperado', 'error'); return; } this.showTaskPreview(); this.showImportStatus(`${this.pendingTasks.length} tareas encontradas`, 'success'); } classifyTask(title, description) { const text = (title + ' ' + description).toLowerCase(); if (text.includes('bug') || text.includes('error') || text.includes('fix')) { return 'bug'; } else if (text.includes('crear') || text.includes('nuevo') || text.includes('implementar')) { return 'historia'; } else if (text.includes('mover') || text.includes('modificar') || text.includes('actualizar')) { return 'mejora'; } else { return 'historia'; } } showTaskPreview() { if (!this.elements.taskList || !this.elements.taskPreview) return; this.elements.taskList.innerHTML = ''; this.pendingTasks.forEach((task, index) => { const taskElement = this.createTaskPreviewElement(task, index); this.elements.taskList.appendChild(taskElement); }); this.elements.taskPreview.style.display = 'block'; } createTaskPreviewElement(task, index) { const div = document.createElement('div'); div.className = 'task-item'; div.innerHTML = `
Task #${task.id}
${task.title}: ${task.description}
${task.type}
`; return div; } editTask(index) { // Simple inline editing const task = this.pendingTasks[index]; const newTitle = prompt('Título de la tarea:', task.title); if (newTitle && newTitle.trim()) { task.title = newTitle.trim(); this.showTaskPreview(); } } removeTask(index) { this.pendingTasks.splice(index, 1); this.showTaskPreview(); this.showImportStatus(`${this.pendingTasks.length} tareas restantes`, 'success'); } confirmTaskImport() { this.state.tasks.push(...this.pendingTasks); this.saveState(); this.showSuccess( 'Importación exitosa', `${this.pendingTasks.length} tareas importadas correctamente al backlog` ); setTimeout(() => { this.cancelTaskImport(); }, 2000); // Announce to screen readers this.announceToScreenReader(`${this.pendingTasks.length} tareas importadas exitosamente`); } cancelTaskImport() { this.pendingTasks = []; this.elements.taskPreview.style.display = 'none'; this.elements.importStatus.style.display = 'none'; this.elements.txtFileInput.value = ''; } showImportStatus(message, type) { if (!this.elements.importStatus) return; this.elements.importStatus.textContent = message; this.elements.importStatus.className = `import-status ${type}`; this.elements.importStatus.style.display = 'block'; } // Demo System initializeDemoSystem() { this.demoStates = { poker: null, retro: null, backlog: null }; // Initialize demo triggers const demoPokerBtn = document.getElementById('demoPoker'); const demoRetroBtn = document.getElementById('demoRetro'); const heroDemoBtn = document.getElementById('heroDemoBtn'); if (demoPokerBtn) { demoPokerBtn.addEventListener('click', () => this.openDemoOverlay('poker')); } if (demoRetroBtn) { demoRetroBtn.addEventListener('click', () => this.openDemoOverlay('retro')); } if (heroDemoBtn) { heroDemoBtn.addEventListener('click', () => this.openDemoOverlay('poker')); } } openDemoOverlay(tab = 'poker') { if (!this.elements.demoOverlay) return; this.elements.demoOverlay.setAttribute('aria-hidden', 'false'); document.body.style.overflow = 'hidden'; this.switchDemoTab(tab); // Focus management setTimeout(() => { const firstTab = this.elements.demoOverlay.querySelector('.demo-tab'); if (firstTab) firstTab.focus(); }, 100); } closeDemoOverlay() { if (!this.elements.demoOverlay) return; this.elements.demoOverlay.setAttribute('aria-hidden', 'true'); document.body.style.overflow = ''; } switchDemoTab(tabName) { // Update tab states this.elements.tabButtons.forEach(tab => { const isActive = tab.id === `tab${tabName.charAt(0).toUpperCase() + tabName.slice(1)}`; tab.classList.toggle('is-active', isActive); tab.setAttribute('aria-selected', isActive); }); // Update panel states this.elements.demoPanels.forEach(panel => { const isActive = panel.id === `panel${tabName.charAt(0).toUpperCase() + tabName.slice(1)}`; panel.classList.toggle('is-active', isActive); panel.hidden = !isActive; }); // Initialize the specific demo this.initializeDemo(tabName); } initializeDemo(type) { switch (type) { case 'poker': this.initializePokerDemo(); break; case 'retro': this.initializeRetroDemo(); break; case 'backlog': this.initializeBacklogDemo(); break; } } initializePokerDemo() { const root = document.getElementById('pokerDemoRoot'); if (!root) return; root.innerHTML = `

Crear Sala de Poker Planning

`; } launchPokerRoom() { const root = document.getElementById('pokerDemoRoot'); if (!root) return; const roomName = document.getElementById('roomName')?.value || 'Demo Room'; const facilitator = document.getElementById('facilitator')?.value || 'Demo User'; root.innerHTML = `

${roomName}

how_to_vote votando
Facilitador: ${facilitator}
Código: 0GVZ5R

Historia actual: Filtrar productos por categoría

Como usuario, quiero poder filtrar productos por categoría para encontrar más fácilmente lo que busco.
Criterios de aceptación: Filtros visibles en la interfaz, posibilidad de múltiple selección, resultados actualizados en tiempo real.

Participantes (4)

✓ 0 / 1 votaron
A
Ana (Tú)
C
Carlos ?
S
Sofia ?
M
Miguel ?

Tu voto

0
1
2
3
5
8
13
21
34
55
89
?
`; this.updateDemoStatus('Sala creada - Selecciona tu voto'); } selectCard(cardElement, value) { // Remove previous selection document.querySelectorAll('.vote-card').forEach(card => { card.classList.remove('is-selected'); }); // Select current card cardElement.classList.add('is-selected'); // Update vote status const voteStatus = document.querySelector('.vote-status'); if (voteStatus) { voteStatus.innerHTML = '✓ 1 / 1 votaron'; } // Update participant status const userChip = document.querySelector('.participant-chip:first-child'); if (userChip) { userChip.classList.add('voted'); userChip.innerHTML = `
A
Ana (Tú) ${value} `; } this.updateDemoStatus(`Voto seleccionado: ${value} - Esperando otros participantes`); this.announceToScreenReader(`Voto ${value} seleccionado`); // Show toast notification this.showInfo('Voto registrado', `Has votado "${value}" para esta historia`); } revealVotes() { const resultsDiv = document.getElementById('pResults'); if (!resultsDiv) return; // Update room status const roomStatus = document.querySelector('.room-status'); if (roomStatus) { roomStatus.className = 'room-status analyzing'; roomStatus.innerHTML = ` analytics analizando `; } // Simulate vote results from real app const results = [ { name: 'Ana', vote: '3', avatar: 'A' }, { name: 'Carlos', vote: '5', avatar: 'C' }, { name: 'Sofia', vote: '5', avatar: 'S' }, { name: 'Miguel', vote: '8', avatar: 'M' } ]; // Update participants with revealed votes const participantsList = document.querySelector('.participants-list'); if (participantsList) { participantsList.innerHTML = results.map(r => `
${r.avatar}
${r.name}${r.name === 'Ana' ? ' (Tú)' : ''} ${r.vote}
`).join(''); } // Show results section resultsDiv.innerHTML = `

Resultados:

warning Se requiere discusión
Pedro 3
Ana, Tú 5
Carlos, María 8
Análisis:

Hay variación significativa en los votos (3-8 puntos). Se recomienda discutir las diferencias de estimación antes de finalizar. Los votos muy dispersos indican diferentes interpretaciones de la complejidad.

`; resultsDiv.style.display = 'block'; this.updateDemoStatus('Votos revelados - Analiza los resultados y decide'); this.showInfo('Votos revelados', 'Se detectó variación en las estimaciones'); } finalizeStory() { this.updateDemoStatus('Historia estimada en 5 puntos - Lista para siguiente'); this.announceToScreenReader('Historia finalizada con 5 puntos de historia'); this.showSuccess('Historia finalizada', 'Estimación completada con 5 puntos de historia'); // Add to state const story = { id: Date.now(), title: 'Filtrar productos por categoría', points: 5, status: 'estimated', participants: ['Ana', 'Carlos', 'Sofia', 'Miguel'] }; if (!this.state.stories) this.state.stories = []; this.state.stories.push(story); this.saveState(); } revoteStory() { const resultsDiv = document.getElementById('pResults'); if (resultsDiv) { resultsDiv.style.display = 'none'; } this.updateDemoStatus('Nueva ronda de votación - Selecciona tu voto'); } skipStory() { this.updateDemoStatus('Historia omitida - Continúa con la siguiente'); } resetPokerDemo() { this.initializePokerDemo(); this.updateDemoStatus('Demo reiniciado'); } initializeRetroDemo() { const root = document.getElementById('retroDemoRoot'); if (!root) return; root.innerHTML = `

Retrospectiva Sprint 15

Went Well

2
Prueba de Columna
favorite 1

To Improve

1
se mejoraron los tiempo
favorite 1

Actions

1
se tomaron acciones en los pases a producción
favorite 1
edit Modo: Edición
`; this.updateDemoStatus('Retrospectiva iniciada - Agrega notas y vota por las más importantes'); } // Retrospective functions addRetroNote(column) { const inputId = column === 'went-well' ? 'wentWellInput' : column === 'to-improve' ? 'toImproveInput' : 'actionsInput'; const input = document.getElementById(inputId); if (!input || !input.value.trim()) return; const noteText = input.value.trim(); const columnElement = document.querySelector(`.retro-column.${column} .note-list`); if (columnElement) { const noteElement = document.createElement('div'); noteElement.className = 'note'; noteElement.innerHTML = `
${noteText}
favorite 0
`; columnElement.appendChild(noteElement); input.value = ''; // Update count const countElement = document.querySelector(`.retro-column.${column} .column-count`); if (countElement) { const currentCount = parseInt(countElement.textContent) || 0; countElement.textContent = currentCount + 1; } this.showSuccess('Nota agregada', `Nueva nota añadida a ${column.replace('-', ' ')}`); } } toggleVote(button) { const isVoted = button.classList.contains('voted'); const voteCount = button.parentElement.querySelector('.vote-count'); const countSpan = voteCount.querySelector('span:last-child'); if (isVoted) { button.classList.remove('voted'); const currentCount = parseInt(countSpan.textContent) || 0; const newCount = Math.max(0, currentCount - 1); countSpan.textContent = newCount; if (newCount === 0) { voteCount.classList.remove('has-votes'); } } else { button.classList.add('voted'); const currentCount = parseInt(countSpan.textContent) || 0; const newCount = currentCount + 1; countSpan.textContent = newCount; voteCount.classList.add('has-votes'); } this.updateDemoStatus(`Voto ${isVoted ? 'removido' : 'agregado'}`); } toggleReaction(button, emoji) { button.classList.toggle('active'); this.updateDemoStatus(`Reacción ${emoji} ${button.classList.contains('active') ? 'agregada' : 'removida'}`); } switchRetroMode(mode) { const modeButtons = document.querySelectorAll('.mode-button'); modeButtons.forEach(btn => btn.classList.remove('active')); const targetButton = Array.from(modeButtons).find(btn => btn.textContent.toLowerCase().includes(mode) || btn.onclick?.toString().includes(mode) ); if (targetButton) { targetButton.classList.add('active'); } const modeBadge = document.querySelector('.retro-mode-badge'); if (modeBadge) { modeBadge.className = `retro-mode-badge ${mode}`; modeBadge.innerHTML = ` ${mode === 'voting' ? 'how_to_vote' : 'edit'} Modo: ${mode === 'voting' ? 'Votación' : 'Edición'} `; } this.updateDemoStatus(`Modo cambiado a ${mode === 'voting' ? 'Votación' : 'Edición'}`); } exportRetro(type) { const exportTypes = { 'board': 'Vista de tablero', 'voting': 'Resultados de votación', 'export': 'Exportar datos' }; this.showInfo('Exportación', `${exportTypes[type]} - Funcionalidad demo`); this.updateDemoStatus(`Exportando: ${exportTypes[type]}`); } voteNote(element) { const currentVotes = parseInt(element.textContent) || 0; const newVotes = currentVotes + 1; element.textContent = newVotes; element.classList.add('is-active'); this.updateDemoStatus(`Voto agregado - Total: ${newVotes}`); this.announceToScreenReader(`Voto agregado, total ${newVotes} votos`); // Auto-sort notes by votes (simplified) setTimeout(() => this.sortNotesByVotes(), 500); } addNote(column) { const noteText = prompt('Escribe tu nota:'); if (!noteText || !noteText.trim()) return; const columnNames = { 'well': '¿Qué funcionó bien?', 'improve': '¿Qué mejorar?', 'action': 'Acciones' }; this.updateDemoStatus(`Nota agregada en "${columnNames[column]}"`); this.announceToScreenReader(`Nueva nota agregada en columna ${columnNames[column]}`); } sortNotesByVotes() { // Simple visual feedback for sorting document.querySelectorAll('.note-list').forEach(list => { const notes = Array.from(list.querySelectorAll('.note')); notes.sort((a, b) => { const votesA = parseInt(a.querySelector('.vote-dot').textContent) || 0; const votesB = parseInt(b.querySelector('.vote-dot').textContent) || 0; return votesB - votesA; }); notes.forEach(note => { list.appendChild(note); // Re-append in sorted order }); }); } initializeBacklogDemo() { const root = document.getElementById('backlogDemoRoot'); if (!root) return; const tasks = this.state.tasks.length > 0 ? this.state.tasks : this.getDefaultBacklogTasks(); root.innerHTML = `

Gestión de Backlog

assignment ${tasks.length} tareas
trending_up ${tasks.filter(t => t.points).reduce((sum, t) => sum + (t.points || 0), 0)} puntos

Backlog ${tasks.filter(t => t.status === 'backlog').length}

${this.renderStoryCards(tasks.filter(t => t.status === 'backlog'))}

Sprint Actual ${tasks.filter(t => t.status === 'sprint').length}

${this.renderStoryCards(tasks.filter(t => t.status === 'sprint'))}

Completado ${tasks.filter(t => t.status === 'done').length}

${this.renderStoryCards(tasks.filter(t => t.status === 'done'))}
`; this.updateDemoStatus('Backlog cargado - Arrastra tareas entre columnas'); this.initializeDragAndDrop(); } getDefaultBacklogTasks() { return [ { id: 105, title: 'Mover botón de lead', description: 'Recolocar el botón a la izquierda', type: 'mejora', status: 'backlog', points: 3 }, { id: 115, title: 'Crear tabla de modificación Lead', description: 'Crear tabla para leads activos', type: 'historia', status: 'backlog', points: 8 }, { id: 113, title: 'Mover botón de Oportunidad', description: 'Colocarlo al final del formulario', type: 'mejora', status: 'sprint', points: 2 }, { id: 181, title: 'Crear Happy Path para gestiones COM', description: 'Preparar demo con happy path', type: 'historia', status: 'sprint', points: 5 }, { id: 112, title: 'Crear integración movimiento local', description: 'Realizar la integración LOCAL', type: 'historia', status: 'done', points: 13 } ]; } renderStoryCards(stories) { return stories.map(story => `
${story.type} ${story.points || '?'}
${story.title}
#${story.id} drag_handle
`).join(''); } initializeDragAndDrop() { const cards = document.querySelectorAll('.story-card'); const columns = document.querySelectorAll('.story-list'); cards.forEach(card => { card.addEventListener('dragstart', (e) => { e.dataTransfer.setData('text/plain', card.dataset.storyId); card.classList.add('dragging'); }); card.addEventListener('dragend', () => { card.classList.remove('dragging'); }); }); columns.forEach(column => { column.addEventListener('dragover', (e) => { e.preventDefault(); column.classList.add('drag-over'); }); column.addEventListener('dragleave', () => { column.classList.remove('drag-over'); }); column.addEventListener('drop', (e) => { e.preventDefault(); const storyId = e.dataTransfer.getData('text/plain'); const newStatus = column.dataset.status; this.moveStory(storyId, newStatus); column.classList.remove('drag-over'); }); }); } moveStory(storyId, newStatus) { const taskIndex = this.state.tasks.findIndex(t => t.id == storyId); if (taskIndex !== -1) { this.state.tasks[taskIndex].status = newStatus; this.saveState(); this.initializeBacklogDemo(); // Refresh the view this.updateDemoStatus(`Tarea #${storyId} movida a ${newStatus}`); this.announceToScreenReader(`Tarea ${storyId} movida a ${newStatus}`); this.showInfo('Tarea movida', `Tarea #${storyId} movida a ${newStatus}`); } } importDemoTasks() { const demoTasks = [ { id: 201, title: 'Implementar autenticación SSO', description: 'Integrar Single Sign-On con Azure AD', type: 'historia', status: 'backlog', points: 8 }, { id: 202, title: 'Optimizar consultas de base de datos', description: 'Mejorar rendimiento de queries complejas', type: 'mejora', status: 'backlog', points: 5 } ]; this.state.tasks.push(...demoTasks); this.saveState(); this.initializeBacklogDemo(); this.updateDemoStatus(`${demoTasks.length} tareas demo importadas`); } planSprint() { const backlogTasks = this.state.tasks.filter(t => t.status === 'backlog'); const selectedTasks = backlogTasks.slice(0, 3); // Take first 3 for demo selectedTasks.forEach(task => { task.status = 'sprint'; }); this.saveState(); this.initializeBacklogDemo(); this.updateDemoStatus(`Sprint planificado con ${selectedTasks.length} tareas`); this.showSuccess('Sprint planificado', `${selectedTasks.length} tareas movidas al sprint actual`); } resetCurrentDemo() { const activePanel = document.querySelector('.demo-panel.is-active'); if (!activePanel) return; const demoType = activePanel.id.replace('panel', '').toLowerCase(); this.initializeDemo(demoType); this.updateDemoStatus('Demo reiniciado'); } updateDemoStatus(message) { const statusElement = document.getElementById('demoStatus'); if (statusElement) { statusElement.textContent = message; } } // Carousel functionality initializeCarousel() { const carousel = document.getElementById('capabilitiesCarousel'); if (!carousel) return; const track = carousel.querySelector('.carousel-track'); const cards = track.querySelectorAll('.card'); const prevBtn = carousel.querySelector('.prev'); const nextBtn = carousel.querySelector('.next'); let currentIndex = 0; const cardWidth = cards[0].offsetWidth + 20; // Include gap const updateCarousel = () => { track.style.transform = `translateX(-${currentIndex * cardWidth}px)`; }; const nextSlide = () => { currentIndex = (currentIndex + 1) % cards.length; updateCarousel(); }; const prevSlide = () => { currentIndex = (currentIndex - 1 + cards.length) % cards.length; updateCarousel(); }; if (nextBtn) nextBtn.addEventListener('click', nextSlide); if (prevBtn) prevBtn.addEventListener('click', prevSlide); // Auto-advance carousel setInterval(nextSlide, 5000); // Touch/swipe support for mobile let startX = 0; let endX = 0; track.addEventListener('touchstart', (e) => { startX = e.touches[0].clientX; }); track.addEventListener('touchend', (e) => { endX = e.changedTouches[0].clientX; const diffX = startX - endX; if (Math.abs(diffX) > 50) { // Minimum swipe distance if (diffX > 0) { nextSlide(); } else { prevSlide(); } } }); } // Scroll effects initializeScrollEffects() { const header = document.querySelector('.header'); if (!header) return; let lastScrollY = window.scrollY; let ticking = false; const updateScrollEffects = () => { const scrollY = window.scrollY; if (scrollY > 100) { header.classList.add('scrolled'); } else { header.classList.remove('scrolled'); } lastScrollY = scrollY; ticking = false; }; const requestScrollUpdate = () => { if (!ticking) { requestAnimationFrame(updateScrollEffects); ticking = true; } }; window.addEventListener('scroll', requestScrollUpdate, { passive: true }); } // Accessibility helpers announceToScreenReader(message) { const announcement = document.createElement('div'); announcement.setAttribute('aria-live', 'polite'); announcement.setAttribute('aria-atomic', 'true'); announcement.style.position = 'absolute'; announcement.style.left = '-10000px'; announcement.style.width = '1px'; announcement.style.height = '1px'; announcement.style.overflow = 'hidden'; document.body.appendChild(announcement); announcement.textContent = message; setTimeout(() => { document.body.removeChild(announcement); }, 1000); } // Utility methods debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } throttle(func, limit) { let inThrottle; return function() { const args = arguments; const context = this; if (!inThrottle) { func.apply(context, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; } // Toast Notification System showToast(title, message, type = 'info', duration = 5000) { const container = document.getElementById('toastContainer'); if (!container) return; const toastId = 'toast-' + Date.now(); const icons = { success: 'check_circle', error: 'error', warning: 'warning', info: 'info' }; const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.id = toastId; toast.innerHTML = ` ${icons[type] || icons.info}
${title}
${message}
`; container.appendChild(toast); // Trigger animation setTimeout(() => { toast.classList.add('show'); }, 10); // Auto-remove after duration setTimeout(() => { this.closeToast(toastId); }, duration); // Announce to screen readers this.announceToScreenReader(`${title}: ${message}`); return toastId; } closeToast(toastId) { const toast = document.getElementById(toastId); if (!toast) return; toast.classList.remove('show'); setTimeout(() => { if (toast.parentNode) { toast.parentNode.removeChild(toast); } }, 300); } // Enhanced feedback methods showSuccess(title, message) { return this.showToast(title, message, 'success'); } showError(title, message) { return this.showToast(title, message, 'error'); } showWarning(title, message) { return this.showToast(title, message, 'warning'); } showInfo(title, message) { return this.showToast(title, message, 'info'); } } // Initialize the application let ceremoApp; document.addEventListener('DOMContentLoaded', () => { ceremoApp = new CeremoApp(); console.log('Ceremo App initialized successfully'); // Mascot interaction const mascot = document.querySelector('.mascot-logo'); if (mascot) { let clickCount = 0; const easterEggMessages = [ "¡Guau! 🐕 ¡Listx para estimar!", "🎯 ¡Planning Poker es divertido!", "🚀 ¡Equipos ágiles, estimaciones precisas!", "💎 ¡DoSoft hace que todo sea mejor!", "🎉 ¡Sigue así, champion!" ]; mascot.addEventListener('click', () => { clickCount++; const message = easterEggMessages[Math.floor(Math.random() * easterEggMessages.length)]; // Create floating message const floatingMsg = document.createElement('div'); floatingMsg.textContent = message; floatingMsg.style.cssText = ` position: fixed; top: 160px; right: 160px; background: var(--color-primary); color: white; padding: 8px 16px; border-radius: 20px; font-size: 0.8rem; font-weight: 500; z-index: 1000; animation: mascotMessage 3s ease-out forwards; pointer-events: none; box-shadow: 0 4px 12px rgba(0,0,0,0.15); `; // Add animation keyframes if not exists if (!document.getElementById('mascotAnimation')) { const style = document.createElement('style'); style.id = 'mascotAnimation'; style.textContent = ` @keyframes mascotMessage { 0% { opacity: 0; transform: translateY(10px) scale(0.8); } 20% { opacity: 1; transform: translateY(0) scale(1); } 80% { opacity: 1; transform: translateY(-5px) scale(1); } 100% { opacity: 0; transform: translateY(-20px) scale(0.9); } } `; document.head.appendChild(style); } document.body.appendChild(floatingMsg); // Remove message after animation setTimeout(() => { if (floatingMsg.parentNode) { floatingMsg.parentNode.removeChild(floatingMsg); } }, 3000); // Special effect after 5 clicks if (clickCount === 5) { mascot.style.animation = 'float 1s ease-in-out 3, mascotCelebrate 0.5s ease-in-out 2'; setTimeout(() => { mascot.style.animation = 'float 6s ease-in-out infinite'; }, 2500); } }); // Add celebrate animation const celebrateStyle = document.createElement('style'); celebrateStyle.textContent = ` @keyframes mascotCelebrate { 0%, 100% { transform: scale(1) rotate(0deg); } 25% { transform: scale(1.1) rotate(-5deg); } 75% { transform: scale(1.1) rotate(5deg); } } `; document.head.appendChild(celebrateStyle); } }); // Enhanced utility namespace window.CeremoDemo = window.CeremoDemo || {}; (function(NS){ NS.util = { randomCode:()=> Math.random().toString(36).substring(2,8).toUpperCase(), safeLS:(action,key,val)=>{ try{ if(action==='get') return localStorage.getItem(key); if(action==='set') localStorage.setItem(key,val); } catch(_) { return null; } }, announce: (msg)=>{ const live=document.getElementById('liveAnnouncer'); if(live){ live.textContent=''; setTimeout(()=> live.textContent=msg,40); } }, formatDate: (date) => { return new Intl.DateTimeFormat('es-ES', { year: 'numeric', month: 'short', day: 'numeric' }).format(new Date(date)); }, generateId: () => { return Math.random().toString(36).substr(2, 9); } }; })(window.CeremoDemo);