diff --git a/manifest.json b/manifest.json index 09448f4..af274f5 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Planification", - "version": "2026.5.34", + "version": "2026.5.35", "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.", "browser_specific_settings": { "gecko": { diff --git a/viewer.css b/viewer.css index bb6a376..5fcd850 100644 --- a/viewer.css +++ b/viewer.css @@ -2771,15 +2771,9 @@ header.topbar::before { filter: brightness(1.1); } /* v2026.5.18 : couleurs par catégorie (fond = couleur, texte blanc) */ -.pinned-popup-dock-pill.color-livraison { background: var(--c-livraison); color: white; border-color: transparent; } -.pinned-popup-dock-pill.color-installation { background: var(--c-installation); color: white; border-color: transparent; } -.pinned-popup-dock-pill.color-recup { background: var(--c-recup); color: white; border-color: transparent; } -.pinned-popup-dock-pill.color-remplacement { background: var(--c-remplacement); color: white; border-color: transparent; } -.pinned-popup-dock-pill.color-incident { background: var(--c-incident); color: white; border-color: transparent; } -.pinned-popup-dock-pill.color-rollout { background: var(--c-rollout); color: white; border-color: transparent; } -.pinned-popup-dock-pill.color-reservation { background: var(--c-reservation); color: white; border-color: transparent; } -.pinned-popup-dock-pill.color-absence { background: #2a2f36; color: white; border-color: transparent; } -.pinned-popup-dock-pill.color-autre { background: var(--c-autre); color: white; border-color: transparent; } +/* v2026.5.26 : les anciennes règles color-XXX qui mettaient un fond coloré vif + sont remplacées par une simple barre verticale à gauche de la pastille. + Les styles pour ::before sont plus bas dans le fichier. */ /* v2026.5.18 : bouton "Fermer tous" à droite du dock */ .pinned-popups-close-all { @@ -3003,3 +2997,644 @@ 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); } +} + +/* ========================================================================== + v2026.5.25 : pastille dock enrichie (lieu + service + date) + + bouton Paramètres dans popup user-badge + + ref dans mini-menu pill + v2026.5.26 : couleurs sobres (fond sombre + barre colorée à gauche) + + contenu centré + ========================================================================== */ + +/* Pastille dock : 3 lignes centrées, fond sombre, barre colorée à gauche */ +.pinned-popup-dock-pill { + flex-direction: column !important; + align-items: center !important; + justify-content: center !important; + padding: 6px 14px 6px 18px !important; + gap: 2px !important; + line-height: 1.2 !important; + text-align: center; + min-width: 200px; + max-width: 300px; + /* Fond sobre et texte bien lisible, peu importe la catégorie */ + background: var(--bg-muted) !important; + color: var(--text) !important; + border: 1px solid var(--border) !important; + position: relative; + overflow: hidden; +} + +/* Barre verticale colorée à gauche = indicateur de catégorie */ +.pinned-popup-dock-pill::before { + content: ""; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 4px; + background: var(--c-autre); +} +.pinned-popup-dock-pill.color-livraison::before { background: var(--c-livraison); } +.pinned-popup-dock-pill.color-installation::before { background: var(--c-installation); } +.pinned-popup-dock-pill.color-recup::before { background: var(--c-recup); } +.pinned-popup-dock-pill.color-remplacement::before { background: var(--c-remplacement); } +.pinned-popup-dock-pill.color-incident::before { background: var(--c-incident); } +.pinned-popup-dock-pill.color-rollout::before { background: var(--c-rollout); } +.pinned-popup-dock-pill.color-reservation::before { background: var(--c-reservation); } +.pinned-popup-dock-pill.color-absence::before { background: #666; } +.pinned-popup-dock-pill.color-autre::before { background: var(--c-autre); } + +.pinned-popup-dock-pill-lieu { + display: block; + font-size: 13px; + font-weight: 700; + max-width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: var(--text); + text-align: center; +} +.pinned-popup-dock-pill-service { + display: block; + font-size: 11px; + font-weight: 500; + color: var(--text-muted); + max-width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-align: center; +} +.pinned-popup-dock-pill-date { + display: block; + font-size: 10px; + font-weight: 500; + color: var(--text-faint); + font-family: var(--mono, monospace); + text-align: center; +} + +/* Ref dans le mini-menu hover de la pastille */ +.pill-hover-menu-ref { + padding: 6px 12px; + text-align: center; + font-family: var(--mono, monospace); + font-size: 13px; + font-weight: 700; + color: var(--text); + border-bottom: 1px solid var(--border); + margin-bottom: 4px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Bouton Paramètres dans popup user-badge */ +.user-name-popup-settings { + display: flex; + align-items: center; + gap: 8px; + margin-top: 8px; + padding: 6px 10px; + background: var(--bg-muted); + color: var(--text); + border: 1px solid var(--border); + border-radius: 6px; + font-family: inherit; + font-size: 13px; + font-weight: 500; + cursor: pointer; + width: 100%; + justify-content: center; + transition: background 0.12s, color 0.12s, border-color 0.12s; +} +.user-name-popup-settings:hover { + background: var(--accent, #3b82f6); + color: white; + border-color: var(--accent, #3b82f6); +} +.user-name-popup-settings .settings-ico { + font-size: 15px; +} + +/* ========================================================================== + v2026.5.26 : rond gris avec "?" quand user inconnu + ========================================================================== */ +.user-badge.user-badge-unknown { + --user-badge-color: #6b7280 !important; + opacity: 0.75; + font-weight: 500; +} +.user-badge.user-badge-unknown:hover { + opacity: 1; +} + +/* ========================================================================== + v2026.5.27 : agrandir +20% les textes topbar + stats bar pour la lisibilité + ========================================================================== */ +/* Labels boutons topbar */ +.btn-action-label, +.btn-refresh-label { + font-size: 14px !important; /* +20% depuis 12px */ +} +.btn-today { + font-size: 14px !important; + padding: 7px 12px !important; +} +.btn-subtle { + font-size: 14px !important; +} +.capture-info { + font-size: 14px !important; /* +20% depuis 12px */ +} +.topbar h1 { + font-size: 21px !important; /* +20% depuis 18px */ +} +/* Date-custom label (Vendredi 24.04.2026) */ +#date-custom-label { + font-size: 14px !important; /* +20% depuis 12px */ +} +.date-custom { + font-size: 14px !important; +} +/* Stats bar */ +.stats-bar, +.stats-bar * { + font-size: 14px !important; +} +.stats-bar strong { + font-size: 16px !important; +} + +/* v2026.5.27 : icône thème plus contrastée avec bordure + fond visible */ +#theme-toggle { + border: 1.5px solid var(--border-strong, rgba(128,128,128,0.5)) !important; + background: var(--bg-muted) !important; + width: 38px; + height: 38px; + padding: 0 !important; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.15s, border-color 0.15s; +} +#theme-toggle:hover { + background: var(--accent-soft, rgba(59, 130, 246, 0.15)) !important; + border-color: var(--accent, #3b82f6) !important; +} +#theme-icon { + font-size: 20px; + line-height: 1; + filter: drop-shadow(0 1px 2px rgba(0,0,0,0.3)); +} + +/* ========================================================================== + v2026.5.27 : classification visuelle des absences (Maladie, Congé, Pompier) + ========================================================================== */ + +/* Variables de couleurs pour les catégories d'absence */ +:root { + --c-maladie: #4338ca; /* Indigo foncé */ + --c-maladie-soft: #e0e7ff; /* Indigo très clair */ + --c-conge: #06b6d4; /* Cyan */ + --c-conge-soft: #cffafe; /* Cyan très clair */ +} +html.theme-dark { + --c-maladie: #818cf8; /* Indigo plus clair pour dark mode */ + --c-maladie-soft: #2a1e66; /* Indigo foncé pour fonds */ + --c-conge: #67e8f9; /* Cyan plus clair pour dark mode */ + --c-conge-soft: #0e3e4a; /* Cyan foncé pour fonds */ +} + +/* Badge "Maladie" à côté du nom */ +.badge-maladie { + background: var(--c-maladie-soft); + color: var(--c-maladie); + font-weight: 600; +} + +/* Badge "Congé" / "Congés" à côté du nom */ +.badge-conge { + background: var(--c-conge-soft); + color: var(--c-conge); + font-weight: 600; +} + +/* Carte entière : couleur de fond + barre gauche épaisse pour absence */ +.card.absence-cat-maladie { + border-left: 4px solid var(--c-maladie); + background: linear-gradient(to bottom, var(--c-maladie-soft) 0%, var(--bg-elevated) 40%); +} +html.theme-dark .card.absence-cat-maladie { + background: linear-gradient(to bottom, rgba(67, 56, 202, 0.12) 0%, var(--bg-elevated) 40%); +} + +.card.absence-cat-conge { + border-left: 4px solid var(--c-conge); + background: linear-gradient(to bottom, var(--c-conge-soft) 0%, var(--bg-elevated) 40%); +} +html.theme-dark .card.absence-cat-conge { + background: linear-gradient(to bottom, rgba(6, 182, 212, 0.12) 0%, var(--bg-elevated) 40%); +} + +/* Pompier — on reprend le style existant mais on accentue le border-left */ +.card.absence-cat-pompier { + border-left: 4px solid var(--danger); +} + +/* Pastille ronde à côté du nom */ +.tech-name-dot { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 50%; + margin-right: 8px; + flex-shrink: 0; + vertical-align: middle; +} +.tech-name-dot.absence-dot-maladie { background: var(--c-maladie); } +.tech-name-dot.absence-dot-conge { background: var(--c-conge); } +.tech-name-dot.absence-dot-pompier { background: var(--danger); } + +/* Note statut (banner texte sur la zone interventions) */ +.card-status-note { + padding: 14px 16px; + font-size: 15px; + font-weight: 500; + text-align: center; + border-radius: 6px; + margin: 10px; +} +.card-status-note.absent-maladie { + background: var(--c-maladie-soft); + color: var(--c-maladie); +} +.card-status-note.absent-conge { + background: var(--c-conge-soft); + color: var(--c-conge); +} +.card-status-note.pompier { + background: var(--danger-soft); + color: var(--danger); +} + +/* ========================================================================== + v2026.5.28 : popups épinglés gardent une taille standard au resize de la + fenêtre. Le max-width 620px du tooltip de base les contraignait quand on + réduisait la largeur depuis le côté droit — désormais ils restent à leur + taille créée et sont simplement repositionnés dans la safe area. + ========================================================================== */ +.pinned-popup:not(.pinned-popup-minimized):not(.pinned-popup-reduced) { + max-width: none !important; + width: 520px !important; /* largeur standard fixe */ + min-width: 380px; +} +/* Sur les très petits écrans (< 600px), on laisse le clamp naturel */ +@media (max-width: 600px) { + .pinned-popup:not(.pinned-popup-minimized):not(.pinned-popup-reduced) { + width: calc(100vw - 20px) !important; + min-width: 0; + } +} + +/* ========================================================================== + v2026.5.30 : absences récurrentes (Pillonel vendredi) en cyan + (même couleur que Congé mais texte distinct "Absent le vendredi") + ========================================================================== */ + +/* Badge "Absent" cyan pour récurrent */ +.badge-recurring { + background: var(--c-conge-soft); + color: var(--c-conge); + font-weight: 600; +} + +/* Carte entière : bordure gauche cyan + fond dégradé */ +.card.absence-cat-recurring { + border-left: 4px solid var(--c-conge); + background: linear-gradient(to bottom, var(--c-conge-soft) 0%, var(--bg-elevated) 40%); +} +html.theme-dark .card.absence-cat-recurring { + background: linear-gradient(to bottom, rgba(6, 182, 212, 0.12) 0%, var(--bg-elevated) 40%); +} + +/* Message "Absent le vendredi" (ou autre récurrence) en cyan */ +.tech-absence-recurring { + padding: 14px 12px; + text-align: center; + font-size: 15px; + font-weight: 500; + font-style: normal; + color: var(--c-conge); + background: var(--c-conge-soft); + border-radius: 6px; + margin: 10px; +} +html.theme-dark .tech-absence-recurring { + background: rgba(6, 182, 212, 0.12); +} + +/* ========================================================================== + v2026.5.30 : mode compact pour écrans 24" Full HD (1920×1080) ou plus petits + Objectif : réduire les paddings et tailles sans casser la lisibilité. + ========================================================================== */ +@media (max-width: 1920px) { + /* Topbar légèrement compactée */ + .topbar { + padding: 8px 14px !important; + gap: 10px; + } + .topbar h1 { + font-size: 18px !important; + } + .app-clock-date, + .app-clock-time { + font-size: 19px !important; + } + .app-clock-date::after { + font-size: 22px !important; + } + + /* Stats bar plus dense */ + .stats-bar { + padding: 6px 12px !important; + } + .stats-bar, + .stats-bar * { + font-size: 13px !important; + } + .stats-bar strong { + font-size: 14px !important; + } + + /* Cartes : padding réduit */ + .card-header { + padding: 8px 12px !important; + } + .card-tech-name { + font-size: 14px !important; + } + .card-tech-badge { + font-size: 10px !important; + padding: 2px 6px !important; + } + + /* Interventions : padding vertical réduit */ + .intervention-v2 { + padding: 7px 10px 9px 6px !important; + } + + /* Timeline plus fine */ + .timeline { + padding: 10px 12px 6px 12px !important; + } + .timeline-bar { + height: 18px !important; + } + .timeline-label { + font-size: 10px !important; + } + + /* Boutons topbar un peu plus compacts */ + .btn-action, + .btn-refresh { + padding: 6px 10px !important; + } + .btn-action-label, + .btn-refresh-label { + font-size: 13px !important; + } + .btn-today { + padding: 6px 10px !important; + font-size: 13px !important; + } + .btn-subtle { + padding: 6px 10px !important; + font-size: 13px !important; + } + + /* Grid cartes : colonnes légèrement plus étroites pour en caser 1-2 de + */ + .main-grid { + gap: 10px !important; + } +} + +/* Breakpoint encore plus étroit (tablette / laptop 13-14") */ +@media (max-width: 1400px) { + .topbar { + padding: 6px 10px !important; + } + .topbar h1 { + font-size: 17px !important; + } + .app-clock-date, + .app-clock-time { + font-size: 17px !important; + } + .intervention-v2 { + padding: 6px 8px 7px 5px !important; + } + .card-header { + padding: 7px 10px !important; + } + .stats-bar, + .stats-bar * { + font-size: 12px !important; + } +} + +/* ========================================================================== + v2026.5.32 : Vue horizontale — chaque tech = 1 ligne fine empilée + Active seulement quand . + But : voir les 8 techs d'un coup sur un 24" Full HD. + ========================================================================== */ + +/* Mode horizontal : la grille devient un simple stack vertical */ +html.view-horizontal .cards { + display: flex !important; + flex-direction: column !important; + gap: 6px !important; + padding: 8px 12px 24px 12px !important; +} + +/* Chaque carte devient une ligne horizontale compacte */ +html.view-horizontal .card { + flex-direction: row !important; + align-items: stretch !important; + height: auto !important; + min-height: 0 !important; + max-height: none !important; + overflow: visible !important; +} + +/* Header devient une barre latérale gauche fixe */ +/* v2026.5.35 : réduit à 140px (au lieu de 200px) pour donner plus de place à la timeline */ +html.view-horizontal .card-header { + flex-direction: column !important; + align-items: flex-start !important; + justify-content: center !important; + min-width: 140px !important; + max-width: 140px !important; + border-bottom: none !important; + border-right: 1px solid var(--border) !important; + padding: 6px 10px !important; + gap: 3px !important; + flex: 0 0 auto; +} +html.view-horizontal .card-tech-name { + font-size: 13px !important; + font-weight: 600; + line-height: 1.2 !important; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + max-width: 100%; +} +html.view-horizontal .card-tech-badge { + font-size: 10px !important; + padding: 2px 6px !important; + white-space: nowrap; +} + +/* Le body prend le reste de la ligne, scroll horizontal si trop d'interv */ +html.view-horizontal .card-body { + flex: 1 1 auto; + min-width: 0; + overflow: hidden; + display: flex; + flex-direction: column; +} + +/* Timeline visible, un peu plus fine */ +html.view-horizontal .timeline { + padding: 6px 10px 4px 10px !important; + background: transparent !important; + border-bottom: none !important; +} +html.view-horizontal .timeline-bar { + height: 22px !important; +} + +/* Liste interventions en mode "chips" (défilement horizontal) */ +html.view-horizontal .card-body > .intervention-v2, +html.view-horizontal .card-body > .intervention { + display: none !important; /* masquer la liste détaillée en vue horiz */ +} + +/* Messages "Pas d'intervention planifiée" / "Absent" tiennent sur la ligne */ +html.view-horizontal .card-empty, +html.view-horizontal .card-status-note, +html.view-horizontal .tech-absence-recurring { + padding: 8px 12px !important; + font-size: 13px !important; + margin: 4px 8px !important; + text-align: left !important; +} + +/* Stats quick pour chaque tech (nb interv etc.) affichées dans le header */ +html.view-horizontal .card-header::after { + content: ""; +} +html.view-horizontal .tech-row-stats { + display: flex; + gap: 8px; + font-size: 11px; + color: var(--text-muted); + margin-top: 2px; +} +html.view-horizontal .tech-row-stats .stat-pill { + padding: 1px 6px; + background: var(--bg-muted); + border: 1px solid var(--border); + border-radius: 3px; + font-variant-numeric: tabular-nums; +} + +/* En vue classique, on cache les éléments spécifiques horizontal */ +html.view-classic .tech-row-stats { + display: none !important; +} + +/* ========================================================================== + v2026.5.35 : en vue horizontale, stats globales sur le CÔTÉ GAUCHE + (colonne verticale fixe) au lieu d'être au-dessus des cartes. + Libère de la hauteur verticale pour les 8 techs. + ========================================================================== */ +html.view-horizontal main { + display: flex; + flex-direction: row; + align-items: stretch; +} + +html.view-horizontal .stats { + flex: 0 0 auto; + width: 200px; + flex-direction: column !important; + align-items: flex-start !important; + gap: 8px !important; + padding: 12px 16px !important; + border-right: 1px solid var(--border); + background: var(--bg-muted); + position: sticky; + top: 0; + align-self: flex-start; + min-height: calc(100vh - 80px); + font-size: 12px !important; +} + +html.view-horizontal .stats .global-stat { + display: block; + width: 100%; + padding: 4px 0; +} +html.view-horizontal .stats .global-stat-main b { + font-size: 18px !important; + color: var(--text); +} +/* Masquer les séparateurs "·" en vue verticale */ +html.view-horizontal .stats .global-stat-sep { + display: none !important; +} + +html.view-horizontal .cards { + flex: 1 1 auto; + min-width: 0; /* autorise le shrink si grand nombre de techs */ +} + +/* Breakpoint étroit : stats plus compactes */ +@media (max-width: 1400px) { + html.view-horizontal .stats { + width: 160px; + padding: 8px 12px !important; + font-size: 11px !important; + } +} diff --git a/viewer.js b/viewer.js index 902a864..478c966 100644 --- a/viewer.js +++ b/viewer.js @@ -7819,15 +7819,26 @@ function _clampPopupInSafeArea(popup) { const vTop = rect.top; // Calcul des coords viewport cibles après clamp + // v2026.5.28 : si le popup est plus large que la zone dispo (fenêtre rétrécie + // depuis le côté droit), on le garde à sa position actuelle — + // l'user redimensionnera ou bougera manuellement. On préfère + // "déborder" à droite plutôt que rétrécir le popup. let newVLeft = vLeft; let newVTop = vTop; - if (newVLeft < safe.left) newVLeft = safe.left; - if (newVLeft + w > safe.right) newVLeft = safe.right - w; - if (newVLeft < safe.left) newVLeft = safe.left; // si popup plus large que viewport - - if (newVTop < safe.top) newVTop = safe.top; - if (newVTop + h > safe.bottom) newVTop = safe.bottom - h; - if (newVTop < safe.top) newVTop = safe.top; + const safeWidth = safe.right - safe.left; + const safeHeight = safe.bottom - safe.top; + if (w <= safeWidth) { + // Popup rentre : on clamp normalement + if (newVLeft < safe.left) newVLeft = safe.left; + if (newVLeft + w > safe.right) newVLeft = safe.right - w; + if (newVLeft < safe.left) newVLeft = safe.left; + } + // Sinon : popup plus large que la zone → on laisse où il est, user libre + if (h <= safeHeight) { + if (newVTop < safe.top) newVTop = safe.top; + if (newVTop + h > safe.bottom) newVTop = safe.bottom - h; + if (newVTop < safe.top) newVTop = safe.top; + } if (newVLeft === vLeft && newVTop === vTop) return; // rien à faire @@ -7849,6 +7860,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. @@ -7870,6 +8060,24 @@ function _formatDateShort(iso) { return `${m[3]}.${m[2]}`; } +/** + * v2026.5.25 : formatte une date ISO en "Mardi 22.04" pour la pastille dock. + */ +function _formatDateForPill(iso) { + if (!iso) return ""; + const m = String(iso).match(/^(\d{4})-(\d{2})-(\d{2})$/); + if (!m) return iso; + try { + const d = new Date(parseInt(m[1]), parseInt(m[2]) - 1, parseInt(m[3])); + const dayName = (typeof DAY_NAMES_FULL !== "undefined" && DAY_NAMES_FULL[d.getDay()]) + ? DAY_NAMES_FULL[d.getDay()] + : ""; + return (dayName ? dayName + " " : "") + m[3] + "." + m[2]; + } catch (e) { + return m[3] + "." + m[2]; + } +} + /** * v2026.5.18 : ajoute (ou met à jour) le bouton "Fermer tous" dans le dock * quand au moins 2 popups épinglés existent (réduits OU affichés). @@ -8244,6 +8452,31 @@ function bindTooltipInteractions() { const el = tooltipEl(); if (!el) return; + // v2026.5.27 : quand la souris entre dans un popup épinglé, fermer tout + // popup non-épinglé (tooltip live ou soft-unpinned) pour garder l'écran clair. + document.addEventListener("mouseover", (e) => { + const target = e.target; + if (!target || !target.closest) return; + const pinned = target.closest(".pinned-popup"); + if (!pinned) return; + // On survole un popup épinglé → fermer tooltip live s'il n'est pas pinned + if (!bulleState.pinned) { + const tip = tooltipEl(); + if (tip && tip.classList.contains("visible")) { + tip.classList.remove("visible"); + tip.classList.add("hidden"); + if (tip.dataset) delete tip.dataset.mode; + state.currentTooltipIv = null; + currentTooltipPos = null; + tooltipPositionMode = null; + } + } + // Fermer les soft-unpinned qui traînent + document.querySelectorAll(".soft-unpinned").forEach(el => { + try { el.remove(); } catch (err) {} + }); + }); + // v4.1.17 : ré-applique la position au scroll de la page (safety net // contre un ancêtre qui casserait position:fixed silencieusement). window.addEventListener("scroll", reapplyTooltipPosition, { passive: true }); @@ -8391,6 +8624,7 @@ function bindTooltipInteractions() { } function buildTooltipHTML(iv) { + if (!iv) return '
Info
'; const i = iv.infobulle || {}; const rows = []; @@ -8589,10 +8823,22 @@ function escapeHtml(s) { } function highlightIntervention(cardEl, ivIdx, on) { - const row = cardEl.querySelector(`.intervention[data-iv-idx="${ivIdx}"]`); + // v2026.5.29 : chercher .intervention-v2 (nouveau nom) et fallback .intervention + const row = cardEl.querySelector(`.intervention-v2[data-iv-idx="${ivIdx}"]`) + || cardEl.querySelector(`.intervention[data-iv-idx="${ivIdx}"]`); const slot = cardEl.querySelector(`.timeline-slot[data-iv-idx="${ivIdx}"]`); if (row) row.classList.toggle("highlight", on); if (slot) slot.classList.toggle("highlight", on); + + // v2026.5.29 : quand on active le highlight (typiquement depuis un slot + // timeline), faire scroller la row dans la vue pour que l'user voie la + // carte correspondante sans avoir à chercher. On évite de scroller le + // body, on scroll juste la row à l'intérieur de la carte si elle déborde. + if (on && row && typeof row.scrollIntoView === "function") { + try { + row.scrollIntoView({ block: "nearest", inline: "nearest", behavior: "smooth" }); + } catch (e) {} + } } // ============================================================================