Files
flowdeck/static/js/app.js
T
bruno e2a739c4c6
FlowDeck CI / test (push) Failing after 5s
FlowDeck CI / docker (push) Has been skipped
feat: responsive mobile UI/UX — hamburger menu, touch support, adaptive layout
- CSS: 3 breakpoints (480px / 768px / 1024px) avec mobile-first design
- Hamburger button dans la topbar (visible ≤768px)
- Sidebar slide-in avec overlay + bouton close
- Kanban scroll-snap columns full-width sur mobile
- Tables scrollables horizontalement
- Touch targets 44px minimum
- Swipe-left to close sidebar
- Modal full-width sur ≤480px
- Topbar simplifiée sur mobile (cacher breadcrumbs center)
- Filter/toolbar wrap + scroll
- Donut/team-load redimensionnés
- app.js: touch events, htmx sidebar close, modal body scroll lock
2026-07-11 00:15:24 -04:00

183 lines
5.5 KiB
JavaScript

// FlowDeck Notion UI — Client-side logic
// Alpine.js + SortableJS + HTMX + Mobile support
(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);
});
})();