// FlowDeck Notion UI — Client-side logic // Alpine.js + SortableJS + HTMX + Mobile support // ── Frontend Error Capture ────────────────────────────────── // Intercepte TOUTES les erreurs JS et les envoie au backend. // Hermes peut les consulter via GET /api/frontend-errors. (function() { 'use strict'; const MAX_ERRORS = 50; var _fd_errors = []; var _fd_throttle = 0; function _fd_send(error) { _fd_errors.push(error); if (_fd_errors.length > MAX_ERRORS) _fd_errors.shift(); // Throttle: max 1 POST per second var now = Date.now(); if (now - _fd_throttle < 1000) return; _fd_throttle = now; try { var xhr = new XMLHttpRequest(); xhr.open('POST', '/api/frontend-error', true); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.send(JSON.stringify(error)); } catch(e) { /* fail silently */ } } window.addEventListener('error', function(e) { _fd_send({ type: 'error', message: e.message || 'Unknown error', source: e.filename || '', line: e.lineno || 0, col: e.colno || 0, stack: (e.error && e.error.stack) ? String(e.error.stack).substring(0, 2000) : '', url: window.location.href, userAgent: navigator.userAgent.substring(0, 200), time: new Date().toISOString() }); }); window.addEventListener('unhandledrejection', function(e) { _fd_send({ type: 'unhandledrejection', message: e.reason ? (e.reason.message || String(e.reason)) : 'Promise rejected', stack: (e.reason && e.reason.stack) ? String(e.reason.stack).substring(0, 2000) : '', url: window.location.href, userAgent: navigator.userAgent.substring(0, 200), time: new Date().toISOString() }); }); // Expose pour le debug console window.__flowdeck_errors = _fd_errors; })(); (function() { 'use strict'; // Theme persistence const savedTheme = localStorage.getItem('theme'); if (savedTheme) { document.documentElement.setAttribute('data-theme', savedTheme); } // Close context menu on click outside document.addEventListener('click', function(e) { const menu = document.getElementById('context-menu'); if (menu && menu.classList.contains('visible')) { if (!menu.contains(e.target)) { menu.classList.remove('visible'); } } }); // Close context menu on escape document.addEventListener('keydown', function(e) { if (e.key === 'Escape') { const menu = document.getElementById('context-menu'); if (menu) menu.classList.remove('visible'); } }); // Tooltip system document.addEventListener('mouseover', function(e) { const target = e.target.closest('[data-tooltip]'); if (!target) return; const tip = document.getElementById('tooltip'); if (!tip) return; tip.textContent = target.getAttribute('data-tooltip'); tip.style.display = 'block'; const rect = target.getBoundingClientRect(); tip.style.left = rect.left + 'px'; tip.style.top = (rect.bottom + 4) + 'px'; }); document.addEventListener('mouseout', function(e) { const target = e.target.closest('[data-tooltip]'); if (!target) return; const tip = document.getElementById('tooltip'); if (tip) tip.style.display = 'none'; }); // Keyboard shortcuts document.addEventListener('keydown', function(e) { // Ctrl+O: New chat (placeholder) if (e.ctrlKey && e.key === 'o') { e.preventDefault(); console.log('New chat shortcut'); } }); // ── Mobile: Swipe to close sidebar ── let touchStartX = 0; let touchStartY = 0; document.addEventListener('touchstart', function(e) { touchStartX = e.touches[0].clientX; touchStartY = e.touches[0].clientY; }, { passive: true }); document.addEventListener('touchend', function(e) { const sidebar = document.getElementById('sidebar'); if (!sidebar || !sidebar.classList.contains('mobile-open')) return; const touchEndX = e.changedTouches[0].clientX; const touchEndY = e.changedTouches[0].clientY; const dx = touchEndX - touchStartX; const dy = Math.abs(touchEndY - touchStartY); // Swipe left on sidebar (>50px horizontal, not too vertical) if (dx < -50 && dy < Math.abs(dx) * 0.5) { // Close sidebar via Alpine store const appLayout = document.querySelector('.app-layout'); if (appLayout && appLayout.__x) { appLayout.__x.$data.mobileSidebarOpen = false; appLayout.__x.$data.sidebarCollapsed = true; } } }); // ── Mobile: close sidebar on page navigation ── document.addEventListener('htmx:beforeRequest', function() { const appLayout = document.querySelector('.app-layout'); if (appLayout && appLayout.__x) { appLayout.__x.$data.mobileSidebarOpen = false; } }); // ── Mobile: passive scroll performance ── if ('ontouchstart' in window) { document.querySelectorAll('.kanban-container').forEach(function(el) { el.style.webkitOverflowScrolling = 'touch'; }); document.querySelectorAll('.table-container').forEach(function(el) { el.style.webkitOverflowScrolling = 'touch'; }); } // Modal functions window.openModal = function(title) { const modal = document.getElementById('card-modal'); const titleEl = document.getElementById('card-modal-title'); if (modal && titleEl) { titleEl.textContent = title || 'Détail'; modal.style.display = 'flex'; // Prevent body scroll when modal is open document.body.style.overflow = 'hidden'; } }; window.closeModal = function() { const modal = document.getElementById('card-modal'); if (modal) { modal.style.display = 'none'; document.body.style.overflow = ''; } }; // Close modal on overlay click document.addEventListener('click', function(e) { if (e.target.id === 'card-modal') { closeModal(); } }); // Close modal on escape document.addEventListener('keydown', function(e) { if (e.key === 'Escape') { closeModal(); } }); // HTMX event handlers document.addEventListener('htmx:afterSwap', function(evt) { // Reinitialize Sortable after HTMX swaps if (evt.target.id === 'kanban-root' || evt.target.closest('.kanban-container')) { initSortable(); } }); // Initialize SortableJS on kanban columns function initSortable() { document.querySelectorAll('.kanban-cards').forEach(function(el) { if (el._sortable) return; // Already initialized el._sortable = new Sortable(el, { group: 'kanban', animation: 200, ghostClass: 'sortable-ghost', dragClass: 'sortable-drag', // Touch support delay: 0, touchStartThreshold: 5, onEnd: function(evt) { const cardId = evt.item.dataset.cardId; const fromCol = evt.from.dataset.status; const toCol = evt.to.dataset.status; const group = evt.to.dataset.group; console.log('Moved card', cardId, 'from', fromCol, 'to', toCol, 'group', group); } }); }); } // Init on page load document.addEventListener('DOMContentLoaded', function() { initSortable(); }); // Re-init when Alpine initializes document.addEventListener('alpine:initialized', function() { setTimeout(initSortable, 100); }); })();