diff --git a/manifest.json b/manifest.json
index 21663d0..eceaa58 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Planification",
- "version": "2026.5.16",
+ "version": "2026.5.17",
"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 0f2adb2..c16db86 100644
--- a/viewer.css
+++ b/viewer.css
@@ -323,17 +323,53 @@ html, body {
flex-wrap: nowrap;
}
-/* v2026.5.16 : nom court du jour (Mardi, Lundi, ...) à gauche du date-picker */
-.date-picker-day {
+/* v2026.5.17 : faux input date custom avec nom du jour */
+.date-custom-wrapper {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+}
+.date-custom {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 5px 10px 5px 12px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: var(--bg-muted);
+ color: var(--text);
+ font-family: inherit;
font-size: 13px;
font-weight: 500;
- color: var(--text-muted);
- padding: 0 6px 0 2px;
- min-width: 58px;
- text-align: right;
+ cursor: pointer;
white-space: nowrap;
user-select: none;
+ transition: border-color 0.15s, background 0.15s;
}
+.date-custom:hover {
+ border-color: var(--border-strong);
+ background: var(--bg-hover);
+}
+.date-custom:focus {
+ outline: 2px solid var(--accent);
+ outline-offset: -1px;
+}
+.date-custom-icon {
+ font-size: 13px;
+ opacity: 0.7;
+}
+.date-input-hidden {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ width: 1px;
+ height: 1px;
+ opacity: 0;
+ pointer-events: none;
+}
+
+/* v2026.5.17 : masquer l'ancien date-picker-day s'il traîne (compat) */
+.date-picker-day { display: none; }
.btn-nav {
padding: 6px 10px;
@@ -979,6 +1015,12 @@ html, body {
opacity: 0;
transition: opacity 0.1s, background 0.1s, color 0.1s;
font-family: inherit;
+ /* v2026.5.17 : figer largeur/hauteur pour que le changement 📋 → ✓ pendant
+ la copie ne fasse pas bouger le titre centré dans la grid */
+ min-width: 28px;
+ min-height: 22px;
+ text-align: center;
+ box-sizing: border-box;
}
.intervention-v2:hover .intervention-copy { opacity: 1; }
.intervention-copy:hover {
@@ -2498,3 +2540,194 @@ header.topbar::before {
.btn-today { padding: 4px 6px; font-size: 11px; }
.btn-nav { min-width: 26px; padding: 4px 6px; }
}
+
+/* ==========================================================================
+ v2026.5.17 : topbar des popups épinglés (3 boutons : _ ▭ 📍)
+ ========================================================================== */
+.pinned-popup {
+ /* Laisser un peu de place en haut pour la topbar */
+ padding-top: 30px !important;
+}
+.pinned-popup-topbar {
+ position: absolute;
+ top: 4px;
+ right: 4px;
+ display: flex;
+ gap: 2px;
+ align-items: center;
+ z-index: 2;
+}
+.pinned-popup-btn {
+ width: 26px;
+ height: 22px;
+ padding: 0;
+ font-size: 13px;
+ line-height: 1;
+ background: transparent;
+ color: var(--text-muted);
+ border: 1px solid transparent;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: background 0.1s, color 0.1s, border-color 0.1s;
+ font-family: inherit;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+.pinned-popup-btn:hover {
+ background: var(--bg-muted);
+ color: var(--text);
+ border-color: var(--border);
+}
+.pinned-popup-unpin {
+ font-size: 14px;
+}
+
+/* ==========================================================================
+ v2026.5.17 : mode Minimisé (popup flottant compact, juste la ref)
+ ========================================================================== */
+.pinned-popup.pinned-popup-minimized {
+ min-width: 160px !important;
+ max-width: 220px !important;
+ width: auto !important;
+ height: auto !important;
+ padding-top: 28px !important;
+ padding-bottom: 6px !important;
+ overflow: hidden;
+}
+.pinned-popup.pinned-popup-minimized > :not(.pinned-popup-topbar):not(.iv-ref-header):not(.pinned-popup-dragbar) {
+ display: none !important;
+}
+.pinned-popup.pinned-popup-minimized .iv-ref-header {
+ text-align: center;
+ padding: 4px 8px !important;
+ grid-column: unset !important;
+ font-size: 14px;
+}
+
+/* ==========================================================================
+ v2026.5.17 : mode Réduit (docké en bas de l'écran) + taskbar
+ ========================================================================== */
+.pinned-popup.pinned-popup-reduced {
+ display: none !important;
+}
+.pinned-popups-dock {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 50;
+ display: none;
+ flex-wrap: wrap;
+ gap: 6px;
+ padding: 6px 10px;
+ background: var(--bg-elevated);
+ border-top: 1px solid var(--border);
+ box-shadow: 0 -2px 10px rgba(0,0,0,0.2);
+}
+.pinned-popups-dock.visible {
+ display: flex;
+}
+.pinned-popup-dock-pill {
+ display: inline-flex;
+ align-items: center;
+ padding: 6px 14px;
+ background: var(--accent, #3b82f6);
+ color: white;
+ border: none;
+ border-radius: 16px;
+ font-family: var(--mono, monospace);
+ font-size: 13px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.15s, transform 0.15s;
+}
+.pinned-popup-dock-pill:hover {
+ background: var(--accent-hover, #2563eb);
+ transform: translateY(-1px);
+}
+
+/* ==========================================================================
+ v2026.5.17 : popup user-badge avec ligne session
+ ========================================================================== */
+.user-name-popup-name {
+ font-weight: 600;
+ margin-bottom: 4px;
+}
+.user-name-popup-session {
+ font-size: 12px;
+ font-variant-numeric: tabular-nums;
+ padding-top: 4px;
+ border-top: 1px solid var(--border);
+}
+.user-name-popup-session.session-ok { color: var(--text-muted); }
+.user-name-popup-session.session-warn { color: #f59e0b; font-weight: 600; }
+.user-name-popup-session.session-critical { color: #ef4444; font-weight: 700; }
+
+/* ==========================================================================
+ v2026.5.17 : popup alerte session qui glisse depuis haut-gauche
+ ========================================================================== */
+.session-slide-alert {
+ position: fixed;
+ top: 60px;
+ left: -420px; /* hors écran au départ */
+ width: 380px;
+ max-width: calc(100vw - 40px);
+ padding: 14px 18px;
+ background: var(--bg-elevated);
+ border: 1px solid var(--border);
+ border-left: 4px solid #f59e0b;
+ border-radius: 8px;
+ box-shadow: 0 8px 24px rgba(0,0,0,0.25);
+ z-index: 1000;
+ transition: left 0.28s ease-out, opacity 0.28s;
+ opacity: 0;
+}
+.session-slide-alert.visible {
+ left: 20px;
+ opacity: 1;
+}
+.session-slide-alert.urgent {
+ border-left-color: #ef4444;
+ animation: session-pulse 1.4s ease-in-out infinite;
+}
+@keyframes session-pulse {
+ 0%, 100% { box-shadow: 0 8px 24px rgba(0,0,0,0.25); }
+ 50% { box-shadow: 0 8px 24px rgba(239,68,68,0.5); }
+}
+.session-slide-alert-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--text);
+ margin-bottom: 12px;
+}
+.session-slide-alert-actions {
+ display: flex;
+ gap: 8px;
+ justify-content: flex-end;
+}
+.session-slide-alert-extend,
+.session-slide-alert-later {
+ padding: 6px 14px;
+ font-size: 13px;
+ border-radius: 6px;
+ border: 1px solid var(--border);
+ cursor: pointer;
+ font-family: inherit;
+}
+.session-slide-alert-extend {
+ background: #10b981;
+ color: white;
+ border-color: #10b981;
+ font-weight: 600;
+}
+.session-slide-alert-extend:hover { background: #059669; }
+.session-slide-alert-extend:disabled { opacity: 0.6; cursor: wait; }
+.session-slide-alert-later {
+ background: transparent;
+ color: var(--text-muted);
+}
+.session-slide-alert-later:hover {
+ background: var(--bg-muted);
+ color: var(--text);
+}
diff --git a/viewer.html b/viewer.html
index 4865569..5164111 100644
--- a/viewer.html
+++ b/viewer.html
@@ -16,9 +16,14 @@
Planification
diff --git a/viewer.js b/viewer.js
index 1574930..0f90d75 100644
--- a/viewer.js
+++ b/viewer.js
@@ -242,6 +242,7 @@ async function init() {
initAppClock(); // v5.0.0 : horloge HH:MM au milieu topbar
initAdminMenu(); // v5.0.0 : menu admin caché (5 clics sur titre)
initSessionTimer(); // v5.0.9 : compteur de session EV (tick 1s)
+ initDateCustomPicker(); // v2026.5.17 : faux input date avec jour
// Initialiser la date = aujourd'hui
state.currentDate = todayISO();
@@ -400,7 +401,21 @@ function toggleUserNamePopup() {
return;
}
if (!state.currentUser || !state.currentUser.name) return;
- popup.textContent = state.currentUser.name;
+
+ // v2026.5.17 : afficher aussi le temps restant de la session (MM:SS) avec
+ // une couleur qui dépend du seuil (vert/jaune/rouge).
+ popup.innerHTML = "";
+ const nameEl = document.createElement("div");
+ nameEl.className = "user-name-popup-name";
+ nameEl.textContent = state.currentUser.name;
+ popup.appendChild(nameEl);
+
+ const sessEl = document.createElement("div");
+ sessEl.className = "user-name-popup-session";
+ sessEl.id = "user-name-popup-session";
+ _renderUserPopupSessionLine(sessEl);
+ popup.appendChild(sessEl);
+
popup.classList.remove("hidden");
badge.classList.add("open");
// Positionne juste en dessous de la pastille
@@ -415,6 +430,38 @@ function hideUserNamePopup() {
if (badge) badge.classList.remove("open");
}
+// v2026.5.17 : remplit la ligne "Session : MM:SS" avec couleur selon seuil.
+// Recalcule à chaque appel — appelée aussi par le tick session pour rafraîchir.
+function _renderUserPopupSessionLine(el) {
+ if (!el) return;
+ const remainingMs = _getSessionRemainingMs();
+ if (remainingMs == null) {
+ el.textContent = "Session : —";
+ el.className = "user-name-popup-session";
+ return;
+ }
+ const mins = Math.floor(remainingMs / 60000);
+ const secs = Math.floor((remainingMs % 60000) / 1000);
+ const txt = `Session : ${String(mins).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
+ el.textContent = txt;
+ el.className = "user-name-popup-session";
+ if (remainingMs <= SESSION_CRITICAL_THRESHOLD_MS) {
+ el.classList.add("session-critical");
+ } else if (remainingMs <= SESSION_WARN_THRESHOLD_MS) {
+ el.classList.add("session-warn");
+ } else {
+ el.classList.add("session-ok");
+ }
+}
+
+// v2026.5.17 : récupère en ms le temps restant avant expiration de la session.
+// Retourne null si on ne connaît pas encore (pas de session ouverte).
+function _getSessionRemainingMs() {
+ if (!state.sessionExpireAt) return null;
+ const remaining = state.sessionExpireAt - Date.now();
+ return remaining > 0 ? remaining : 0;
+}
+
// ============================================================================
// Thème clair/sombre
// ============================================================================
@@ -839,21 +886,53 @@ function initAppClock() {
setInterval(tick, 30 * 1000);
}
-// v2026.5.16 : met à jour le label court du jour affiché à gauche du
-// date-picker (ex: "Mardi", "Lundi"). Appelé à chaque changement de date.
+// v2026.5.17 : met à jour le faux input date custom (ex: "Vendredi 24.04.2026")
+// Remplace l'ancien updateDatePickerDayLabel. L'input date natif reste présent
+// mais caché, et son onChange continue de déclencher le chargement.
const DAY_NAMES_FULL = ["Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"];
function updateDatePickerDayLabel(isoDate) {
- const el = document.getElementById("date-picker-day");
+ const el = document.getElementById("date-custom-label");
if (!el) return;
if (!isoDate) { el.textContent = ""; return; }
try {
const d = isoToDate(isoDate);
- el.textContent = DAY_NAMES_FULL[d.getDay()];
+ const day = DAY_NAMES_FULL[d.getDay()];
+ const dd = String(d.getDate()).padStart(2, "0");
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
+ const yyyy = d.getFullYear();
+ el.textContent = `${day} ${dd}.${mm}.${yyyy}`;
} catch (e) {
el.textContent = "";
}
}
+// v2026.5.17 : brancher le faux input date — clic dessus ouvre le vrai input
+// caché pour choisir une date.
+function initDateCustomPicker() {
+ const custom = document.getElementById("date-custom");
+ const picker = document.getElementById("date-picker");
+ if (!custom || !picker) return;
+ const openPicker = () => {
+ try {
+ if (typeof picker.showPicker === "function") {
+ picker.showPicker();
+ } else {
+ picker.focus();
+ picker.click();
+ }
+ } catch (e) {
+ picker.focus();
+ }
+ };
+ custom.addEventListener("click", openPicker);
+ custom.addEventListener("keydown", (e) => {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ openPicker();
+ }
+ });
+}
+
// v5.0.0 : ligne verticale rouge "heure actuelle" sur la timeline, visible
// UNIQUEMENT quand on affiche aujourd'hui. Appelée à chaque tick de l'horloge
// + après chaque render (cf renderFromData).
@@ -1043,6 +1122,103 @@ function updateSessionIndicator() {
}
};
}
+
+ // v2026.5.17 : si le popup user-badge est ouvert, rafraîchir la ligne "Session : MM:SS"
+ const sessLineInPopup = document.getElementById("user-name-popup-session");
+ if (sessLineInPopup) _renderUserPopupSessionLine(sessLineInPopup);
+
+ // v2026.5.17 : popup d'alerte "glissante" depuis le haut gauche
+ // - à 5 min : alerte standard (si pas encore affichée ni "plus tard")
+ // - à 2 min : alerte urgente (si pas encore affichée)
+ _handleSessionSlideAlerts(remainingMs);
+}
+
+/**
+ * v2026.5.17 : gère les 2 alertes popup glissant depuis le haut gauche.
+ * - Première alerte à 5 min (SESSION_WARN_THRESHOLD_MS). Reste affichée jusqu'à
+ * action manuelle (Prolonger ou Plus tard).
+ * - Si "Plus tard", une 2e alerte plus urgente réapparait à 2 min
+ * (SESSION_CRITICAL_THRESHOLD_MS).
+ */
+function _handleSessionSlideAlerts(remainingMs) {
+ if (remainingMs == null) return;
+
+ // Alerte à 5 min
+ if (remainingMs <= SESSION_WARN_THRESHOLD_MS
+ && remainingMs > SESSION_CRITICAL_THRESHOLD_MS
+ && !state._slideAlert5minShown) {
+ state._slideAlert5minShown = true;
+ _showSessionSlideAlert({ urgent: false });
+ }
+
+ // Alerte à 2 min (si déjà "Plus tard" sur l'alerte 5 min OU alerte 5 min jamais affichée)
+ if (remainingMs <= SESSION_CRITICAL_THRESHOLD_MS
+ && !state._slideAlert2minShown) {
+ state._slideAlert2minShown = true;
+ // Cacher éventuellement l'ancienne alerte pour ré-afficher la nouvelle
+ _hideSessionSlideAlert();
+ _showSessionSlideAlert({ urgent: true });
+ }
+}
+
+function _showSessionSlideAlert({ urgent }) {
+ // Retirer l'ancienne si elle existe
+ _hideSessionSlideAlert();
+
+ const el = document.createElement("div");
+ el.id = "session-slide-alert";
+ el.className = "session-slide-alert" + (urgent ? " urgent" : "");
+ const title = urgent ? "⚠ Session expire dans 2 minutes !" : "⏱ Session expire dans 5 minutes";
+ el.innerHTML = `
+ ${title}
+
+
+
+
+ `;
+ document.body.appendChild(el);
+ // Déclenche l'animation de slide-in (petite tempo pour que la transition parte)
+ requestAnimationFrame(() => el.classList.add("visible"));
+
+ // Action "Prolonger"
+ el.querySelector(".session-slide-alert-extend").addEventListener("click", async () => {
+ const extendBtn = el.querySelector(".session-slide-alert-extend");
+ extendBtn.disabled = true;
+ extendBtn.textContent = "…";
+ try {
+ const resp = await sendMessage({ type: "extendSession" });
+ if (resp && resp.ok && typeof resp.remainingMs === "number") {
+ state.sessionExpireAt = Date.now() + resp.remainingMs;
+ state.sessionPingDone = false;
+ state._criticalModalShown = false;
+ // Reset des flags d'alerte pour le prochain cycle
+ state._slideAlert5minShown = false;
+ state._slideAlert2minShown = false;
+ showToast("Session prolongée", "30 minutes de plus");
+ updateSessionIndicator();
+ _hideSessionSlideAlert();
+ } else {
+ throw new Error((resp && resp.error) || "erreur inconnue");
+ }
+ } catch (err) {
+ extendBtn.disabled = false;
+ extendBtn.textContent = "🔄 Prolonger";
+ }
+ });
+
+ // Action "Plus tard"
+ el.querySelector(".session-slide-alert-later").addEventListener("click", () => {
+ _hideSessionSlideAlert();
+ // Si c'est l'alerte 5 min qu'on dismissa, l'alerte 2 min reviendra
+ // automatiquement (state._slideAlert2minShown toujours false).
+ });
+}
+
+function _hideSessionSlideAlert() {
+ const el = document.getElementById("session-slide-alert");
+ if (!el) return;
+ el.classList.remove("visible");
+ setTimeout(() => { try { el.remove(); } catch (e) {} }, 250);
}
/**
@@ -2188,12 +2364,11 @@ async function writeCache(isoDate, data) {
// ============================================================================
async function loadForDate(isoDate, opts = {}) {
- // v4.3.1 : changer de date ferme tous les popups épinglés. Ils réfèrent à
- // des interventions du jour courant, ils n'ont aucun sens sur un autre jour.
+ // v4.3.1 : changer de date fermait tous les popups épinglés.
+ // v2026.5.17 : les popups épinglés restent maintenant ouverts entre dates,
+ // avec les données qu'ils avaient au moment de l'épinglage. L'utilisateur
+ // peut les fermer manuellement s'il veut.
const previousDate = state.currentDate;
- if (previousDate && previousDate !== isoDate && typeof closeAllPinnedPopups === "function") {
- closeAllPinnedPopups();
- }
state.currentDate = isoDate;
document.getElementById("date-picker").value = isoDate;
@@ -5991,7 +6166,8 @@ function hideTooltip(opts = {}) {
state.currentTooltipIv = null;
currentTooltipPos = null;
tooltipPositionMode = null; // re-détecter à la prochaine ouverture
- }, 120);
+ }, 1000); // v2026.5.17 : délai 1s au lieu de 120ms pour laisser le temps
+ // à l'user d'atteindre le popup depuis la carte
}
// v4.2 : détecte si l'utilisateur a une sélection de texte active dans la bulle.
@@ -6125,9 +6301,51 @@ function positionTooltipAnchored(rowEl) {
}
if (y < 4) y = 4;
+ // v2026.5.17 : éviter le chevauchement avec les popups épinglés existants.
+ // On teste la position candidate, et si elle chevauche un popup épinglé,
+ // on essaie d'autres candidats (gauche de la carte, au-dessous, au-dessus).
+ const tipW = tipRect.width || 320;
+ const tipH = tipRect.height || 200;
+ const pinnedRects = _getPinnedPopupsViewportRects();
+ if (pinnedRects.length) {
+ const candidates = [
+ { x, y, label: "right" },
+ { x: rowRect.left - tipW - pad, y: rowRect.top, label: "left" },
+ { x: rowRect.left, y: rowRect.bottom + pad, label: "below" },
+ { x: rowRect.left, y: rowRect.top - tipH - pad, label: "above" }
+ ];
+ for (const c of candidates) {
+ // Borne dans le viewport
+ if (c.x < 4) c.x = 4;
+ if (c.x + tipW > window.innerWidth - 8) c.x = window.innerWidth - tipW - 8;
+ if (c.y < 4) c.y = 4;
+ if (c.y + tipH > window.innerHeight - 8) c.y = window.innerHeight - tipH - 8;
+ const testRect = { left: c.x, top: c.y, right: c.x + tipW, bottom: c.y + tipH };
+ const overlaps = pinnedRects.some(pr => _rectsOverlap(testRect, pr));
+ if (!overlaps) {
+ x = c.x; y = c.y;
+ break;
+ }
+ }
+ }
+
setTooltipViewportPosition(x, y);
}
+/**
+ * v2026.5.17 : retourne les rectangles (en coords viewport) de tous les popups
+ * actuellement épinglés et visibles (non réduits). Utilisé pour anti-chevauchement.
+ */
+function _getPinnedPopupsViewportRects() {
+ const rects = [];
+ document.querySelectorAll(".pinned-popup").forEach(p => {
+ if (p.classList.contains("pinned-popup-reduced")) return; // docké, pas à l'écran
+ const r = p.getBoundingClientRect();
+ if (r.width > 0 && r.height > 0) rects.push(r);
+ });
+ return rects;
+}
+
// ============================================================================
// v4.3.0 : système de popups épinglés détachés
// ============================================================================
@@ -6258,20 +6476,55 @@ function pinTooltip() {
popup.dataset.actionId = iv.actionId || "";
popup.innerHTML = srcEl.innerHTML;
- // Ajouter un bouton × de fermeture (en plus du 📌)
- const closeBtn = document.createElement("button");
- closeBtn.type = "button";
- closeBtn.className = "pinned-popup-close";
- closeBtn.innerHTML = "×";
- closeBtn.title = "Désépingler (reste visible tant que la souris est dessus)";
- closeBtn.addEventListener("click", (e) => {
+ // v2026.5.17 : masquer l'icône 📌 du contenu cloné (redondante car le
+ // popup a sa propre topbar avec le bouton "désépingler" 📍 explicite)
+ const oldPin = popup.querySelector('.tooltip-pinbtn[data-action="pin"]');
+ if (oldPin) oldPin.remove();
+
+ // v2026.5.17 : topbar avec 3 boutons pour un popup épinglé :
+ // _ = Minimiser (popup reste flottant mais compact, juste la ref)
+ // ▭ = Réduire (docké dans la taskbar du bas)
+ // 📍 = Désépingler (l'icône d'épingle "plantée" ; clic = retire l'épingle)
+ const topbar = document.createElement("div");
+ topbar.className = "pinned-popup-topbar";
+
+ // Bouton Minimiser
+ const minBtn = document.createElement("button");
+ minBtn.type = "button";
+ minBtn.className = "pinned-popup-btn pinned-popup-minimize";
+ minBtn.innerHTML = "_";
+ minBtn.title = "Minimiser (reste flottant mais compact)";
+ minBtn.addEventListener("click", (e) => {
+ e.stopPropagation();
+ _minimizePinnedPopup(popup);
+ });
+ topbar.appendChild(minBtn);
+
+ // Bouton Réduire
+ const reduceBtn = document.createElement("button");
+ reduceBtn.type = "button";
+ reduceBtn.className = "pinned-popup-btn pinned-popup-reduce";
+ reduceBtn.innerHTML = "▭";
+ reduceBtn.title = "Réduire (docké en bas de l'écran)";
+ reduceBtn.addEventListener("click", (e) => {
+ e.stopPropagation();
+ _reducePinnedPopup(popup);
+ });
+ topbar.appendChild(reduceBtn);
+
+ // Bouton Désépingler (icône épingle plantée)
+ const unpinBtn = document.createElement("button");
+ unpinBtn.type = "button";
+ unpinBtn.className = "pinned-popup-btn pinned-popup-unpin";
+ unpinBtn.innerHTML = "📍";
+ unpinBtn.title = "Désépingler (se ferme quand la souris sort)";
+ unpinBtn.addEventListener("click", (e) => {
e.stopPropagation();
- // Désépinglage "mou" : on marque la popup comme non épinglée mais on la
- // laisse visible tant que la souris est dessus. Elle disparaît quand la
- // souris sort.
_softUnpinPopup(popup);
});
- popup.appendChild(closeBtn);
+ topbar.appendChild(unpinBtn);
+
+ popup.appendChild(topbar);
// v4.3.3 : barre de drag en haut, pour déplacer la popup à la souris.
// Ancrée en haut à 22px de haut ; le padding-top de la popup est augmenté
@@ -6377,6 +6630,11 @@ function _softUnpinPopup(el) {
if (dragbar) dragbar.remove();
const closeBtn = el.querySelector(".pinned-popup-close");
if (closeBtn) closeBtn.remove();
+ // v2026.5.17 : retirer aussi la nouvelle topbar et le conteneur minimisé
+ const topbar = el.querySelector(".pinned-popup-topbar");
+ if (topbar) topbar.remove();
+ el.classList.remove("pinned-popup-minimized");
+ el.classList.remove("pinned-popup-reduced");
// Helper qui joue l'animation de sortie puis supprime le DOM
const animateAndRemove = () => {
@@ -6393,6 +6651,164 @@ function _softUnpinPopup(el) {
el.addEventListener("mouseleave", animateAndRemove, { once: true });
}
+// ============================================================================
+// v2026.5.17 : États d'un popup épinglé
+// - Normal (complet, flottant)
+// - Minimisé (compact, flottant, juste la ref + topbar)
+// - Réduit (docké dans la taskbar en bas de l'écran)
+// ============================================================================
+
+/**
+ * Passe un popup épinglé en mode Minimisé : on ne montre plus que la ref,
+ * dans un petit cadre flottant toujours drag-able.
+ */
+function _minimizePinnedPopup(popup) {
+ if (!popup) return;
+ popup.classList.add("pinned-popup-minimized");
+
+ // Adapter les boutons topbar : [_] devient [⬆] (agrandir)
+ const minBtn = popup.querySelector(".pinned-popup-minimize");
+ if (minBtn) {
+ minBtn.innerHTML = "⬆";
+ minBtn.title = "Agrandir";
+ // On retire les anciens listeners en clonant l'élément
+ const newBtn = minBtn.cloneNode(true);
+ minBtn.replaceWith(newBtn);
+ newBtn.addEventListener("click", (e) => {
+ e.stopPropagation();
+ _expandPinnedPopup(popup);
+ });
+ }
+
+ // Clic sur la ref (dans iv-ref-header) = agrandir aussi
+ const refEl = popup.querySelector(".iv-ref-header");
+ if (refEl) {
+ refEl.style.cursor = "pointer";
+ refEl.title = "Cliquer pour agrandir";
+ refEl.addEventListener("click", _onMinimizedRefClick);
+ }
+}
+
+function _onMinimizedRefClick(e) {
+ const popup = e.currentTarget.closest(".pinned-popup");
+ if (popup) _expandPinnedPopup(popup);
+}
+
+/**
+ * Repasse un popup minimisé en mode Normal (complet).
+ */
+function _expandPinnedPopup(popup) {
+ if (!popup) return;
+ popup.classList.remove("pinned-popup-minimized");
+
+ // Restaurer bouton Minimiser
+ 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", (e) => {
+ e.stopPropagation();
+ _minimizePinnedPopup(popup);
+ });
+ }
+
+ // Retirer listener du clic-agrandir sur la ref
+ const refEl = popup.querySelector(".iv-ref-header");
+ if (refEl) {
+ refEl.style.cursor = "";
+ refEl.title = "";
+ refEl.removeEventListener("click", _onMinimizedRefClick);
+ }
+}
+
+/**
+ * Passe un popup épinglé en mode Réduit : il disparaît de son emplacement
+ * flottant et vient s'ajouter dans une taskbar en bas de l'écran sous forme
+ * de pastille cliquable.
+ */
+function _reducePinnedPopup(popup) {
+ if (!popup) return;
+
+ // Récupérer la référence pour le label de la pastille
+ const refEl = popup.querySelector(".iv-ref-header");
+ const label = refEl ? (refEl.textContent || "").trim() || "Popup" : "Popup";
+
+ // S'assurer que la taskbar du bas existe
+ let dock = document.getElementById("pinned-popups-dock");
+ if (!dock) {
+ dock = document.createElement("div");
+ dock.id = "pinned-popups-dock";
+ dock.className = "pinned-popups-dock";
+ document.body.appendChild(dock);
+ }
+
+ // Créer la pastille dock
+ const pill = document.createElement("button");
+ pill.type = "button";
+ pill.className = "pinned-popup-dock-pill";
+ pill.textContent = label;
+ pill.title = "Cliquer pour agrandir";
+
+ // Mémoriser la position/taille du popup avant de le masquer
+ const rect = popup.getBoundingClientRect();
+ popup.dataset.prevLeft = popup.style.left || (rect.left + "px");
+ popup.dataset.prevTop = popup.style.top || (rect.top + "px");
+ popup.dataset.prevWidth = popup.style.width || "";
+
+ // Cacher le popup (on le garde en DOM pour conserver son état et restaurer
+ // instantanément)
+ popup.classList.add("pinned-popup-reduced");
+
+ // Associer pill ↔ popup
+ pill._linkedPopup = popup;
+ popup._linkedPill = pill;
+
+ pill.addEventListener("click", (e) => {
+ e.stopPropagation();
+ _restorePinnedPopupFromDock(popup);
+ });
+
+ dock.appendChild(pill);
+ dock.classList.add("visible");
+}
+
+/**
+ * Ramène un popup réduit en mode Normal : retire la pastille du dock et
+ * réaffiche le popup flottant à sa position d'avant réduction.
+ */
+function _restorePinnedPopupFromDock(popup) {
+ if (!popup) return;
+ popup.classList.remove("pinned-popup-reduced");
+ // Si le popup était minimisé avant d'être réduit, on l'agrandit direct
+ // (la demande était : "Si la reduit et rappeller s'affiche en grand direct")
+ popup.classList.remove("pinned-popup-minimized");
+ 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", (e) => {
+ e.stopPropagation();
+ _minimizePinnedPopup(popup);
+ });
+ }
+
+ // Supprimer la pastille associée
+ if (popup._linkedPill) {
+ popup._linkedPill.remove();
+ popup._linkedPill = null;
+ }
+
+ // Si le dock est vide, le masquer
+ const dock = document.getElementById("pinned-popups-dock");
+ if (dock && dock.children.length === 0) {
+ dock.classList.remove("visible");
+ }
+}
+
/** Ferme toutes les popups épinglées (appelé par Échap ou changement de date). */
/**
* v5.0.1 : helper pour déclencher la suppression d'une absence ou réservation.