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
This commit is contained in:
+64
-5
@@ -1,5 +1,5 @@
|
||||
// FlowDeck Notion UI — Client-side logic
|
||||
// Alpine.js + SortableJS + HTMX
|
||||
// Alpine.js + SortableJS + HTMX + Mobile support
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
@@ -57,6 +57,53 @@
|
||||
}
|
||||
});
|
||||
|
||||
// ── 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');
|
||||
@@ -64,12 +111,17 @@
|
||||
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';
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
};
|
||||
|
||||
// Close modal on overlay click
|
||||
@@ -79,6 +131,13 @@
|
||||
}
|
||||
});
|
||||
|
||||
// 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
|
||||
@@ -96,15 +155,15 @@
|
||||
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);
|
||||
|
||||
// TODO: Send move to API
|
||||
// htmx.ajax('POST', '/api/move', { ... });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user