From 6a0324b252824b1c05d7e860b149c3d2c58e94cb Mon Sep 17 00:00:00 2001 From: Quentin Rouiller Date: Fri, 24 Apr 2026 13:22:08 +0200 Subject: [PATCH] =?UTF-8?q?v2026.5.36=20=E2=80=94=20Sidebar=20verticale=20?= =?UTF-8?q?en=20vue=20horizontale=20(#horizontal-wrapper=20[sidebar=20200p?= =?UTF-8?q?x]=20+=20[main])=20[code=20interpol=C3=A9=20entre=20v2026.5.35?= =?UTF-8?q?=20et=20v2026.5.37]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- manifest.json | 2 +- viewer.css | 278 ++++++++++++++++++++++++++++++++++++++++++++------ viewer.js | 188 ++++++++++++++++++++++++++++++++++ 3 files changed, 437 insertions(+), 31 deletions(-) diff --git a/manifest.json b/manifest.json index f50c122..915435c 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Planification", - "version": "2026.5.35", + "version": "2026.5.36", "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 5fcd850..440b1ee 100644 --- a/viewer.css +++ b/viewer.css @@ -3586,55 +3586,273 @@ html.view-classic .tech-row-stats { /* ========================================================================== 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. + v2026.5.36 : REFONTE — les stats (et tout le reste) sont maintenant déplacés + physiquement dans .horizontal-sidebar (via JS _moveElementsToSidebar). + Le CSS ci-dessous est conservé au cas où une ancienne instance + ait encore .stats dans
, mais il ne devrait plus s'appliquer. ========================================================================== */ -html.view-horizontal main { +html.view-horizontal main#main.legacy-stats-layout { display: flex; flex-direction: row; align-items: stretch; } -html.view-horizontal .stats { +/* ========================================================================== + v2026.5.36 : Vue horizontale — sidebar verticale à gauche + Contient (haut → bas) : nav date, horloge, stats, boutons actions. + Seuls restent en haut : user-badge, titre "Planification", bouton thème. + ========================================================================== */ + +/* Topbar en vue horizontale : minimaliste */ +html.view-horizontal .topbar { + padding: 6px 12px !important; + gap: 8px; +} +/* Cacher la zone centrale (déplacée vers sidebar) */ +html.view-horizontal .topbar .app-clock, +html.view-horizontal .topbar .capture-info, +html.view-horizontal .topbar .app-session { + display: none !important; +} +/* topbar-left ne contient plus que user-badge + titre */ +html.view-horizontal .topbar-left { + gap: 10px; +} +/* topbar-right ne contient plus que le theme-toggle */ +html.view-horizontal .topbar-right { + gap: 4px; +} + +/* Sidebar verticale à gauche */ +.horizontal-sidebar { 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); + min-width: 200px; + max-width: 200px; background: var(--bg-muted); + border-right: 1px solid var(--border); + padding: 10px 12px; + display: flex; + flex-direction: column; + gap: 8px; + overflow-y: auto; position: sticky; top: 0; align-self: flex-start; - min-height: calc(100vh - 80px); - font-size: 12px !important; + max-height: calc(100vh - 48px); /* topbar ~48px */ + font-size: 12px; + box-sizing: border-box; } -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 { +/* Cacher la sidebar en vue classique (au cas où elle existe encore) */ +html.view-classic .horizontal-sidebar { display: none !important; } -html.view-horizontal .cards { - flex: 1 1 auto; - min-width: 0; /* autorise le shrink si grand nombre de techs */ +/* Layout main avec sidebar */ +html.view-horizontal main#main { + display: flex; + flex-direction: row; + align-items: stretch; } -/* Breakpoint étroit : stats plus compactes */ +/* Groupe navigation date dans la sidebar */ +html.view-horizontal .horizontal-sidebar .date-nav { + display: flex; + flex-direction: row; + align-items: center; + gap: 4px; + width: 100%; + flex-wrap: wrap; +} +html.view-horizontal .horizontal-sidebar .date-nav .btn-nav { + flex: 0 0 auto; + padding: 4px 8px; + min-width: 28px; +} +html.view-horizontal .horizontal-sidebar .date-nav .date-custom-wrapper { + flex: 1 1 100%; + order: -1; /* date au-dessus des flèches */ +} +html.view-horizontal .horizontal-sidebar .date-nav .date-custom { + width: 100%; + justify-content: flex-start; + padding: 6px 8px; + font-size: 12px !important; +} +html.view-horizontal .horizontal-sidebar .date-nav .btn-today { + flex: 0 0 auto; + padding: 4px 10px; + font-size: 12px !important; +} + +/* Horloge dans la sidebar */ +html.view-horizontal .horizontal-sidebar .app-clock { + position: static !important; + transform: none !important; + left: auto !important; + top: auto !important; + display: flex; + flex-direction: column; + align-items: flex-start; + width: 100%; + gap: 2px; + padding: 6px 8px; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 4px; +} +html.view-horizontal .horizontal-sidebar .app-clock-date { + font-size: 11px !important; + color: var(--text-muted) !important; + font-weight: 500 !important; +} +html.view-horizontal .horizontal-sidebar .app-clock-date::after { + content: "" !important; /* supprimer le gros point · */ + display: none !important; +} +html.view-horizontal .horizontal-sidebar .app-clock-time { + font-size: 20px !important; + font-weight: 700 !important; + color: var(--text) !important; + font-variant-numeric: tabular-nums; +} + +/* Info capture (Synchronisé à...) */ +html.view-horizontal .horizontal-sidebar .capture-info { + display: block !important; + font-size: 11px !important; + color: var(--text-muted); + padding: 2px 4px; +} + +/* Stats globales dans la sidebar (séparateurs cachés) */ +html.view-horizontal .horizontal-sidebar #stats { + display: flex !important; + flex-direction: column !important; + gap: 4px !important; + padding: 6px 4px !important; + border-top: 1px solid var(--border); + border-bottom: 1px solid var(--border); + margin: 2px 0; + background: transparent !important; + width: 100%; + box-sizing: border-box; +} +html.view-horizontal .horizontal-sidebar #stats .global-stat { + display: block; + width: 100%; + padding: 2px 0; + font-size: 11px; + color: var(--text-muted); +} +html.view-horizontal .horizontal-sidebar #stats .global-stat b { + color: var(--text); + font-weight: 700; + font-size: 13px; +} +html.view-horizontal .horizontal-sidebar #stats .global-stat-main b { + font-size: 15px !important; +} +html.view-horizontal .horizontal-sidebar #stats .global-stat-sep { + display: none !important; +} +html.view-horizontal .horizontal-sidebar #stats .global-stat-sub { + display: block !important; + font-size: 10px; + color: var(--text-faint); + padding-left: 6px; +} + +/* Boutons d'action dans la sidebar : pleine largeur, empilés */ +html.view-horizontal .horizontal-sidebar button.btn { + width: 100%; + justify-content: flex-start !important; + padding: 6px 10px !important; + font-size: 12px !important; + gap: 6px; + flex: 0 0 auto; +} +html.view-horizontal .horizontal-sidebar .btn-action-label, +html.view-horizontal .horizontal-sidebar .btn-refresh-label { + display: inline !important; + font-size: 12px !important; +} +html.view-horizontal .horizontal-sidebar .btn-action-icon, +html.view-horizontal .horizontal-sidebar .btn-refresh-icon { + flex: 0 0 16px; + width: 16px; + height: 16px; +} + +/* Grille cards prend le reste de la largeur */ +html.view-horizontal .cards { + flex: 1 1 auto; + min-width: 0; +} + +/* v2026.5.36 : zone nom tech encore plus petite (140 → 120px) */ +html.view-horizontal .card-header { + min-width: 120px !important; + max-width: 120px !important; +} + +/* Annuler les règles plus anciennes v2026.5.35 qui mettaient les stats à gauche + de manière inline (elles sont désormais dans la sidebar unifiée) */ +html.view-horizontal main .stats { + /* Les stats sont maintenant DANS la sidebar, pas dans main. Si pour une + raison quelconque elles y restent, on les cache. */ +} + +/* Breakpoint étroit : sidebar plus fine */ @media (max-width: 1400px) { - html.view-horizontal .stats { - width: 160px; - padding: 8px 12px !important; - font-size: 11px !important; + .horizontal-sidebar { + width: 170px; + min-width: 170px; + max-width: 170px; + padding: 8px 10px; + font-size: 11px; + } + html.view-horizontal .card-header { + min-width: 110px !important; + max-width: 110px !important; } } + +/* ========================================================================== + v2026.5.36 : layout global du body en vue horizontale + La topbar reste en haut (pleine largeur), puis body devient flex-row : + [sidebar] + [main] + ========================================================================== */ +html.view-horizontal body { + display: flex; + flex-direction: column; +} +html.view-horizontal body > header.topbar { + flex: 0 0 auto; +} +html.view-horizontal body { + /* Le body doit permettre à sidebar + main de se placer côte à côte. + On met un wrapper flex via un pseudo-selecteur impossible à hacker + proprement sans JS, donc on utilise display:flex sur body et + flex-direction: row pour tout sauf la topbar et quelques éléments + absolument positionnés (toasts, modals...). Plus simple : + on rend .horizontal-sidebar et main voisins via flex sur un wrapper + JS-créé (plus bas). À défaut, positioning absolute fallback. */ +} + +/* Fallback : si pas de wrapper, on positionne .horizontal-sidebar en sticky + dans le flux normal.
n'est pas juste à côté mais en dessous — + ce n'est pas l'idéal, d'où le wrapper JS. */ + +/* v2026.5.36 : wrapper flex-row pour sidebar + main en vue horizontale */ +.horizontal-wrapper { + display: flex; + flex-direction: row; + align-items: stretch; + width: 100%; +} +html.view-horizontal .horizontal-wrapper > main#main { + flex: 1 1 auto; + min-width: 0; +} diff --git a/viewer.js b/viewer.js index 478c966..24a5777 100644 --- a/viewer.js +++ b/viewer.js @@ -1046,10 +1046,198 @@ function _toggleView() { _setCurrentView(next); } +/** + * v2026.5.36 : applique le mode de vue (classique/horizontal) en déplaçant + * physiquement les éléments de la topbar vers/depuis une sidebar verticale + * à gauche de l'écran. + * + * En vue horizontale : + * - Sidebar gauche verticale contenant (haut → bas) : + * · Navigation date (prev / date / next / aujourd'hui) + * · Horloge + date (compacte, une par ligne) + * · Info de synchro + * · Stats globales (interventions/techs/absents) + * · Boutons actions (Absence, Douchette, Actualiser, Tout recharger, Vider cache) + * - Topbar réduite à : user-badge + titre + theme-toggle + * + * En vue classique : + * - Tout est remis dans la topbar comme avant (topbar-left / topbar-right) + * + * On mémorise les parents d'origine sur chaque élément (data-orig-parent) + * pour restaurer proprement en vue classique. + * + * Logs [viewMode] pour debug. + */ function _applyViewMode() { const mode = _getCurrentView(); + console.log(`[viewMode] application de la vue : ${mode}`); + + // Mettre à jour la classe sur pour les règles CSS document.documentElement.classList.remove("view-classic", "view-horizontal"); document.documentElement.classList.add("view-" + mode); + + // Liste des IDs à déplacer entre topbar (classique) et sidebar (horizontal) + // Ordre = ordre visuel dans la sidebar en mode horizontal (haut → bas) + const ELEMENTS_TO_RELOCATE = [ + "date-nav", // (pas un id mais une classe, traité à part) + "app-clock", + "capture-info", + "stats", // conteneur stats globales (généré dynamiquement) + "absence-btn", + "douchette-btn", + "refresh-partial-btn", + "refresh-btn", + "abort-btn", + "clear-cache-btn" + ]; + + if (mode === "horizontal") { + _moveElementsToSidebar(ELEMENTS_TO_RELOCATE); + } else { + _restoreElementsToTopbar(ELEMENTS_TO_RELOCATE); + } + + console.log(`[viewMode] application terminée (mode=${mode})`); +} + +/** + * v2026.5.36 : crée ou retrouve la sidebar, et y déplace les éléments listés. + * Les éléments sont déplacés dans l'ordre du tableau. + */ +function _moveElementsToSidebar(ids) { + // Créer (ou retrouver) le wrapper qui contiendra sidebar + main côte à côte + let wrapper = document.getElementById("horizontal-wrapper"); + const mainEl = document.getElementById("main"); + if (!wrapper) { + wrapper = document.createElement("div"); + wrapper.id = "horizontal-wrapper"; + wrapper.className = "horizontal-wrapper"; + // Placer le wrapper à la place de
, puis mettre
DEDANS + if (mainEl && mainEl.parentNode) { + mainEl.parentNode.insertBefore(wrapper, mainEl); + wrapper.appendChild(mainEl); + console.log("[viewMode] wrapper créé,
encapsulé"); + } else { + console.warn("[viewMode]
introuvable, wrapper ajouté à "); + document.body.appendChild(wrapper); + } + } + + // Créer la sidebar si elle n'existe pas, et la mettre EN PREMIER dans le wrapper + let sidebar = document.getElementById("horizontal-sidebar"); + if (!sidebar) { + sidebar = document.createElement("aside"); + sidebar.id = "horizontal-sidebar"; + sidebar.className = "horizontal-sidebar"; + wrapper.insertBefore(sidebar, wrapper.firstChild); + console.log("[viewMode] sidebar créée et insérée en tête du wrapper"); + } else if (sidebar.parentNode !== wrapper) { + // Sidebar existe ailleurs, on la remet dans le wrapper + wrapper.insertBefore(sidebar, wrapper.firstChild); + } + + // Traitement spécial : .date-nav (classe, pas id) + const dateNav = document.querySelector(".date-nav"); + if (dateNav) { + _memorizeOriginalParent(dateNav); + sidebar.appendChild(dateNav); + console.log("[viewMode] déplacé dans sidebar : .date-nav"); + } + + // Pour chaque ID listé, trouver et déplacer + for (const id of ids) { + if (id === "date-nav") continue; // déjà traité + const el = document.getElementById(id); + if (!el) { + console.log(`[viewMode] élément #${id} introuvable (optionnel) — skip`); + continue; + } + _memorizeOriginalParent(el); + sidebar.appendChild(el); + console.log(`[viewMode] déplacé dans sidebar : #${id}`); + } +} + +/** + * v2026.5.36 : restaure chaque élément à son parent d'origine (mémorisé dans + * data-orig-parent). Utilisé quand on revient en vue classique. + */ +function _restoreElementsToTopbar(ids) { + // Restaurer .date-nav + const dateNav = document.querySelector(".date-nav"); + if (dateNav) _restoreToOriginalParent(dateNav, ".date-nav"); + + for (const id of ids) { + if (id === "date-nav") continue; + const el = document.getElementById(id); + if (!el) continue; + _restoreToOriginalParent(el, "#" + id); + } + + // Supprimer la sidebar si elle existe et est vide + const sidebar = document.getElementById("horizontal-sidebar"); + if (sidebar && sidebar.children.length === 0) { + sidebar.remove(); + console.log("[viewMode] sidebar supprimée (vide)"); + } + + // Sortir
du wrapper et supprimer le wrapper + const wrapper = document.getElementById("horizontal-wrapper"); + if (wrapper) { + const mainEl = wrapper.querySelector("#main"); + if (mainEl && wrapper.parentNode) { + wrapper.parentNode.insertBefore(mainEl, wrapper); + console.log("[viewMode]
restauré hors du wrapper"); + } + wrapper.remove(); + console.log("[viewMode] wrapper supprimé"); + } +} + +function _memorizeOriginalParent(el) { + if (el.dataset.origParent) return; // déjà mémorisé + const parent = el.parentNode; + if (!parent || !parent.id) { + // Parent sans id : on mémorise par selector (parent tag + classes) + let selector = parent.tagName.toLowerCase(); + if (parent.className) { + const cls = parent.className.split(/\s+/)[0]; + if (cls) selector += "." + cls; + } + el.dataset.origParent = "SEL:" + selector; + } else { + el.dataset.origParent = "ID:" + parent.id; + } + // Mémoriser aussi l'index d'origine pour restaurer l'ordre + const siblings = Array.from(parent.children); + el.dataset.origIndex = String(siblings.indexOf(el)); +} + +function _restoreToOriginalParent(el, label) { + const orig = el.dataset.origParent; + if (!orig) { + console.log(`[viewMode] ${label} n'a pas de parent d'origine mémorisé — skip`); + return; + } + let parent = null; + if (orig.startsWith("ID:")) { + parent = document.getElementById(orig.substring(3)); + } else if (orig.startsWith("SEL:")) { + parent = document.querySelector(orig.substring(4)); + } + if (!parent) { + console.warn(`[viewMode] parent d'origine pour ${label} introuvable (orig=${orig})`); + return; + } + // Insérer à l'index d'origine si possible + const idx = parseInt(el.dataset.origIndex || "0", 10); + const refChild = parent.children[idx] || null; + if (refChild && refChild !== el) { + parent.insertBefore(el, refChild); + } else { + parent.appendChild(el); + } + console.log(`[viewMode] restauré à ${orig} : ${label}`); } // v5.0.0 : horloge HH:MM au milieu de la topbar. Mise à jour toutes les 30s