diff --git a/manifest.json b/manifest.json index 3f52ea1..8032e17 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Planification", - "version": "2026.5.22", + "version": "2026.5.23", "description": "Vue claire et rapide du planning des techniciens EasyVista. Regroupe interventions et réservations par tech, affiche horaires, contact, lieu, catégorie et statut en un coup d'œil.", "permissions": ["activeTab", "scripting", "storage", "tabs", "alarms"], "host_permissions": [ diff --git a/viewer.css b/viewer.css index 15e2ca3..ace9856 100644 --- a/viewer.css +++ b/viewer.css @@ -2593,45 +2593,105 @@ header.topbar::before { v2026.5.19 : refonte — élément .pinned-popup-minref créé à la volée v2026.5.21 : agrandi pour que la ref tienne sans déborder v2026.5.22 : encore agrandi + plus d'espace entre dragbar et topbar + v2026.5.23 : refonte complète en style "onglet" single-line, compact ========================================================================== */ .pinned-popup.pinned-popup-minimized { + display: flex !important; + flex-direction: row !important; + align-items: center !important; min-width: 300px !important; - max-width: 360px !important; - width: 300px !important; - height: auto !important; - min-height: 80px !important; - padding: 44px 16px 16px 16px !important; + max-width: 400px !important; + width: auto !important; + height: 36px !important; + min-height: 36px !important; + padding: 0 6px 0 4px !important; overflow: visible; background: var(--bg-elevated) !important; border: 1px solid var(--border) !important; + border-radius: 6px !important; } -/* Séparer visuellement la dragbar (collée en haut) des boutons topbar */ + +/* Dans le mode minimisé, la topbar n'est plus en absolute : elle se pose en fin + de ligne à droite, après la ref */ .pinned-popup.pinned-popup-minimized .pinned-popup-topbar { - top: 14px !important; /* sous la dragbar (qui fait ~6-8px) */ + position: static !important; + top: auto !important; + right: auto !important; + margin-left: auto !important; + order: 3; + flex-shrink: 0; + padding: 0 2px; } -/* Masquer tous les enfants directs du popup minimisé */ + +/* La dragbar devient un simple "handle" à gauche (≡) */ +.pinned-popup.pinned-popup-minimized .pinned-popup-dragbar { + position: static !important; + top: auto !important; + left: auto !important; + right: auto !important; + order: 1; + flex-shrink: 0; + width: 18px !important; + height: 22px !important; + background: transparent !important; + border: none !important; + cursor: grab; + display: flex !important; + align-items: center; + justify-content: center; + color: var(--text-faint); + opacity: 0.6; + transition: opacity 0.12s, color 0.12s; +} +.pinned-popup.pinned-popup-minimized .pinned-popup-dragbar::before { + content: "≡"; + font-size: 15px; + line-height: 1; +} +.pinned-popup.pinned-popup-minimized .pinned-popup-dragbar:hover { + opacity: 1; + color: var(--text); +} +.pinned-popup.pinned-popup-minimized.dragging .pinned-popup-dragbar { + cursor: grabbing; +} + +/* Masquer tous les enfants directs SAUF topbar, dragbar et minref */ .pinned-popup.pinned-popup-minimized > *:not(.pinned-popup-topbar):not(.pinned-popup-dragbar):not(.pinned-popup-minref) { display: none !important; } -/* L'élément ref dédié, centré et gros */ + +/* La ref au centre, cliquable pour agrandir */ .pinned-popup-minref { - display: block; - text-align: center; - padding: 10px 12px; + display: flex; + align-items: center; + flex: 1; + min-width: 0; + padding: 0 10px; font-family: var(--mono, monospace); - font-size: 15px; + font-size: 14px; font-weight: 700; color: var(--text); cursor: pointer; user-select: none; - border-radius: 4px; - transition: background 0.12s; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + order: 2; } .pinned-popup-minref:hover { - background: var(--bg-muted); + color: var(--accent, #3b82f6); +} + +/* Boutons plus petits en mode minimisé */ +.pinned-popup.pinned-popup-minimized .pinned-popup-btn { + width: 22px !important; + height: 22px !important; + font-size: 12px !important; +} +.pinned-popup.pinned-popup-minimized .pinned-popup-refresh svg { + width: 12px; + height: 12px; } /* ========================================================================== @@ -2910,3 +2970,28 @@ body.popup-dragging .pinned-popup { background: rgba(239, 68, 68, 0.15); border-radius: 4px; } + +/* ========================================================================== + v2026.5.23 : drag & drop des pastilles du dock + ========================================================================== */ +.pinned-popup-dock-pill { + cursor: grab; + user-select: none; + transition: opacity 0.15s, transform 0.12s; +} +.pinned-popup-dock-pill:active { + cursor: grabbing; +} +.pinned-popup-dock-pill.pill-dragging { + opacity: 0.3 !important; + pointer-events: none; +} +.pill-dragging-ghost { + animation: pill-ghost-bounce 0.2s ease-out; + transform: scale(1.05); + box-shadow: 0 10px 24px rgba(0,0,0,0.4); +} +@keyframes pill-ghost-bounce { + from { transform: scale(1); } + to { transform: scale(1.05); } +} diff --git a/viewer.js b/viewer.js index 859823b..86f447e 100644 --- a/viewer.js +++ b/viewer.js @@ -6806,6 +6806,13 @@ function _softUnpinPopup(el) { const idx = pinnedPopups.findIndex(p => p.el === el); if (idx >= 0) pinnedPopups.splice(idx, 1); + // v2026.5.23 : reset le flag global bulleState.pinned si plus aucun popup + // épinglé. Sans ça, le tooltip live "croit" toujours qu'il est pinned et + // son handler click (notamment ↻ reload) peut partir en cacahuète. + if (pinnedPopups.length === 0) { + bulleState.pinned = false; + } + // v4.3.3 corr : basculer visuellement en tooltip normal (retirer tous les // attributs visuels du mode épinglé : bordure bleue, dragbar, bouton ×, // padding-top, etc.). La classe .soft-unpinned fait ça côté CSS. @@ -6847,9 +6854,17 @@ function _softUnpinPopup(el) { // intervention que celle qu'on désépingle, il faut regénérer son HTML pour // que l'icône passe de 📍 (active rouge) à 📌 (non active) — sinon l'user // voit l'ancienne icône et croit qu'il est toujours épinglé. + // v2026.5.23 : AUSSI reset le flag iv._reloading qui pourrait rester bloqué + // à true si le bouton ↻ avait été cliqué pendant que le popup était épinglé + // sans que le finally soit atteint. const tip = tooltipEl(); if (tip && tip.classList.contains("visible") && state.currentTooltipIv) { - tip.innerHTML = buildTooltipHTML(state.currentTooltipIv); + try { + state.currentTooltipIv._reloading = false; + tip.innerHTML = buildTooltipHTML(state.currentTooltipIv); + } catch (err) { + console.warn("[softUnpin] buildTooltipHTML failed:", err); + } } // Helper qui joue l'animation de sortie puis supprime le DOM @@ -7006,12 +7021,18 @@ function _reducePinnedPopup(popup) { popup._linkedPill = pill; pill.addEventListener("click", (e) => { + // v2026.5.23 : si on était en train de drag, ne pas déclencher le clic + if (pill._justDragged) { + pill._justDragged = false; + return; + } e.stopPropagation(); _restorePinnedPopupFromDock(popup); }); // v2026.5.20 : mini-menu au survol (Agrandir / Fermer) pill.addEventListener("mouseenter", () => { + if (pill._dragging) return; // pas de menu pendant le drag _showPillHoverMenu(pill, popup); }); pill.addEventListener("mouseleave", (e) => { @@ -7019,6 +7040,9 @@ function _reducePinnedPopup(popup) { _schedulePillMenuClose(); }); + // v2026.5.23 : drag & drop — réordonne dans le dock, ou restaure hors du dock + _attachPillDragHandler(pill, popup); + dock.appendChild(pill); dock.classList.add("visible"); @@ -7195,6 +7219,185 @@ function _reclampAllFloatingPopups() { }); } +/** + * v2026.5.23 : drag & drop d'une pastille du dock. + * - Drag dans le dock : réordonne les pastilles (drop = insérer à la nouvelle + * position). Les autres se décalent en live. + * - Drop HORS du dock : restaure le popup en mode Normal à la position de la + * souris. + * Semi-transparent pendant le drag. + */ +function _attachPillDragHandler(pill, popup) { + const DRAG_THRESHOLD = 4; // px avant de considérer un vrai drag + let isDown = false; + let isDragging = false; + let startX = 0, startY = 0; + let ghostEl = null; + let pillOriginalNext = null; // voisin d'origine pour restaurer l'ordre + + const onMouseMove = (e) => { + if (!isDown) return; + const dx = e.clientX - startX; + const dy = e.clientY - startY; + if (!isDragging) { + if (Math.abs(dx) < DRAG_THRESHOLD && Math.abs(dy) < DRAG_THRESHOLD) return; + // Début du drag + isDragging = true; + pill._dragging = true; + pill._justDragged = true; + _hidePillHoverMenu(); + pill.classList.add("pill-dragging"); + + // Créer un "ghost" flottant qui suit la souris + const r = pill.getBoundingClientRect(); + ghostEl = pill.cloneNode(true); + ghostEl.style.position = "fixed"; + ghostEl.style.left = r.left + "px"; + ghostEl.style.top = r.top + "px"; + ghostEl.style.width = r.width + "px"; + ghostEl.style.height = r.height + "px"; + ghostEl.style.zIndex = "1100"; + ghostEl.style.opacity = "0.85"; + ghostEl.style.pointerEvents = "none"; + ghostEl.classList.add("pill-dragging-ghost"); + document.body.appendChild(ghostEl); + + // Mémoriser le voisin d'origine pour restaurer si drop hors dock + pillOriginalNext = pill.nextSibling; + } + // Déplacer le ghost avec la souris + if (ghostEl) { + ghostEl.style.left = (e.clientX - ghostEl.offsetWidth / 2) + "px"; + ghostEl.style.top = (e.clientY - ghostEl.offsetHeight / 2) + "px"; + } + + // Détecter si on est au-dessus du dock + const dock = document.getElementById("pinned-popups-dock"); + if (!dock) return; + const dockRect = dock.getBoundingClientRect(); + const insideDock = ( + e.clientX >= dockRect.left && e.clientX <= dockRect.right && + e.clientY >= dockRect.top && e.clientY <= dockRect.bottom + ); + + if (insideDock) { + // Trouver où insérer parmi les autres pastilles + const pills = Array.from(dock.querySelectorAll(".pinned-popup-dock-pill")) + .filter(p => p !== pill); + let inserted = false; + for (const other of pills) { + const or = other.getBoundingClientRect(); + const midX = or.left + or.width / 2; + if (e.clientX < midX) { + dock.insertBefore(pill, other); + inserted = true; + break; + } + } + if (!inserted) { + // Insérer juste avant le bouton "Fermer tous" s'il existe, sinon en fin + const closeAllBtn = document.getElementById("pinned-popups-close-all"); + if (closeAllBtn) { + dock.insertBefore(pill, closeAllBtn); + } else { + dock.appendChild(pill); + } + } + } + }; + + const onMouseUp = (e) => { + if (!isDown) return; + isDown = false; + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("mouseup", onMouseUp); + + if (!isDragging) return; // juste un clic simple, pas un drag + + isDragging = false; + setTimeout(() => { pill._dragging = false; }, 10); + pill.classList.remove("pill-dragging"); + if (ghostEl) { + try { ghostEl.remove(); } catch (err) {} + ghostEl = null; + } + + // Vérifier si drop dans le dock ou hors + const dock = document.getElementById("pinned-popups-dock"); + let insideDock = false; + if (dock) { + const dockRect = dock.getBoundingClientRect(); + insideDock = ( + e.clientX >= dockRect.left && e.clientX <= dockRect.right && + e.clientY >= dockRect.top && e.clientY <= dockRect.bottom + ); + } + + if (insideDock) { + // Drop dans le dock = réordonnage déjà fait pendant onMouseMove. OK. + return; + } + + // Drop HORS du dock : restaurer le popup en mode Normal à la position souris + // Le popup est actuellement en état "réduit" — on le réaffiche. + popup.classList.remove("pinned-popup-reduced"); + popup.classList.remove("pinned-popup-minimized"); + // Positionner à la souris (coords document) + const popupW = popup.offsetWidth || 320; + const popupH = popup.offsetHeight || 200; + const docX = _viewportToDocumentX(e.clientX - popupW / 2); + const docY = _viewportToDocumentY(e.clientY - 20); + popup.style.left = docX + "px"; + popup.style.top = docY + "px"; + // Clamper dans la safe area + _clampPopupInSafeArea(popup); + + // Retirer la pastille et nettoyer le dock si vide + try { pill.remove(); } catch (err) {} + popup._linkedPill = null; + if (dock && dock.querySelectorAll(".pinned-popup-dock-pill").length === 0) { + dock.classList.remove("visible"); + const closeAllBtn = document.getElementById("pinned-popups-close-all"); + if (closeAllBtn) closeAllBtn.remove(); + } else { + _ensureDockCloseAllBtn(); + } + // Restaurer le bouton Minimiser (si c'était en mode minimisé avant réduction) + const minBtn = popup.querySelector(".pinned-popup-minimize"); + if (minBtn) { + minBtn.innerHTML = "▭"; + minBtn.title = "Minimiser (reste flottant mais compact)"; + const newBtn = minBtn.cloneNode(true); + minBtn.replaceWith(newBtn); + newBtn.addEventListener("click", (ev) => { + ev.stopPropagation(); + _minimizePinnedPopup(popup); + }); + } + + // v2026.5.23 : mettre à jour le rect dans pinnedPopups pour que l'anti- + // chevauchement tienne compte de la nouvelle position + const entry = pinnedPopups.find(p => p.el === popup); + if (entry) { + const l = parseFloat(popup.style.left) || 0; + const t = parseFloat(popup.style.top) || 0; + const w = popup.offsetWidth; + const h = popup.offsetHeight; + entry.rect = { left: l, top: t, right: l + w, bottom: t + h }; + } + }; + + pill.addEventListener("mousedown", (e) => { + if (e.button !== 0) return; + isDown = true; + isDragging = false; + startX = e.clientX; + startY = e.clientY; + document.addEventListener("mousemove", onMouseMove); + document.addEventListener("mouseup", onMouseUp); + }); +} + /** * v2026.5.19 : réduit TOUS les popups épinglés actuellement ouverts (en mode * normal ou minimisé) dans la taskbar du bas. Appelé au changement de date. @@ -7737,6 +7940,7 @@ function bindTooltipInteractions() { } function buildTooltipHTML(iv) { + if (!iv) return '