feat: Refonte UI Notion-complete — sidebar, topbar, multi-vues, dark mode
- Layout 3 zones (sidebar 240px + topbar 44px + contenu) - Sidebar: sections hiérarchiques, hover states, menu contextuel - CSS design system Notion: #191919/#222/#333, Inter, coins 4-6px - Vues: Kanban (groupes+colonnes), Dashboard Notion-style - Templates: base.html refondu, card.html partial, board.html v2 - Backend: données sidebar + board Notion (groups/cards) - Docs: ROADMAP v0.4→v1.0, WORKLOAD.md, README - Backward compatible: conserve _issue_column pour api.py - Images de référence + resized (analyse 2026-07-08)
This commit is contained in:
+122
-11
@@ -1,12 +1,123 @@
|
||||
// FlowDeck — Theme toggle
|
||||
(function() {
|
||||
const saved = localStorage.getItem('flowdeck-theme');
|
||||
if (saved) document.documentElement.setAttribute('data-theme', saved);
|
||||
})();
|
||||
// FlowDeck Notion UI — Client-side logic
|
||||
// Alpine.js + SortableJS + HTMX
|
||||
|
||||
function toggleTheme() {
|
||||
const current = document.documentElement.getAttribute('data-theme');
|
||||
const next = current === 'light' ? 'dark' : 'light';
|
||||
document.documentElement.setAttribute('data-theme', next);
|
||||
localStorage.setItem('flowdeck-theme', next);
|
||||
}
|
||||
(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');
|
||||
}
|
||||
});
|
||||
|
||||
// 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';
|
||||
}
|
||||
};
|
||||
|
||||
window.closeModal = function() {
|
||||
const modal = document.getElementById('card-modal');
|
||||
if (modal) modal.style.display = 'none';
|
||||
};
|
||||
|
||||
// Close modal on overlay click
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.id === 'card-modal') {
|
||||
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',
|
||||
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', { ... });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Init on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initSortable();
|
||||
});
|
||||
|
||||
// Re-init when Alpine initializes
|
||||
document.addEventListener('alpine:initialized', function() {
|
||||
setTimeout(initSortable, 100);
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user