v2026.5.36 — Sidebar verticale en vue horizontale (#horizontal-wrapper [sidebar 200px] + [main]) [code interpolé entre v2026.5.35 et v2026.5.37]

This commit is contained in:
Quentin Rouiller
2026-04-24 13:22:08 +02:00
parent fd466504c2
commit 6a0324b252
3 changed files with 437 additions and 31 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "Planification", "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.", "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"], "permissions": ["activeTab", "scripting", "storage", "tabs", "alarms"],
"host_permissions": [ "host_permissions": [
+248 -30
View File
@@ -3586,55 +3586,273 @@ html.view-classic .tech-row-stats {
/* ========================================================================== /* ==========================================================================
v2026.5.35 : en vue horizontale, stats globales sur le CÔTÉ GAUCHE v2026.5.35 : en vue horizontale, stats globales sur le CÔTÉ GAUCHE
(colonne verticale fixe) au lieu d'être au-dessus des cartes. v2026.5.36 : REFONTE — les stats (et tout le reste) sont maintenant déplacés
Libère de la hauteur verticale pour les 8 techs. physiquement dans .horizontal-sidebar (via JS _moveElementsToSidebar).
Le CSS ci-dessous est conservé au cas où une ancienne instance
ait encore .stats dans <main>, mais il ne devrait plus s'appliquer.
========================================================================== */ ========================================================================== */
html.view-horizontal main { html.view-horizontal main#main.legacy-stats-layout {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: stretch; 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; flex: 0 0 auto;
width: 200px; width: 200px;
flex-direction: column !important; min-width: 200px;
align-items: flex-start !important; max-width: 200px;
gap: 8px !important;
padding: 12px 16px !important;
border-right: 1px solid var(--border);
background: var(--bg-muted); 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; position: sticky;
top: 0; top: 0;
align-self: flex-start; align-self: flex-start;
min-height: calc(100vh - 80px); max-height: calc(100vh - 48px); /* topbar ~48px */
font-size: 12px !important; font-size: 12px;
box-sizing: border-box;
} }
html.view-horizontal .stats .global-stat { /* Cacher la sidebar en vue classique (au cas où elle existe encore) */
display: block; html.view-classic .horizontal-sidebar {
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; display: none !important;
} }
html.view-horizontal .cards { /* Layout main avec sidebar */
flex: 1 1 auto; html.view-horizontal main#main {
min-width: 0; /* autorise le shrink si grand nombre de techs */ 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) { @media (max-width: 1400px) {
html.view-horizontal .stats { .horizontal-sidebar {
width: 160px; width: 170px;
padding: 8px 12px !important; min-width: 170px;
font-size: 11px !important; 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. <main> 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;
}
+188
View File
@@ -1046,10 +1046,198 @@ function _toggleView() {
_setCurrentView(next); _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() { function _applyViewMode() {
const mode = _getCurrentView(); const mode = _getCurrentView();
console.log(`[viewMode] application de la vue : ${mode}`);
// Mettre à jour la classe sur <html> pour les règles CSS
document.documentElement.classList.remove("view-classic", "view-horizontal"); document.documentElement.classList.remove("view-classic", "view-horizontal");
document.documentElement.classList.add("view-" + mode); 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 <main>, puis mettre <main> DEDANS
if (mainEl && mainEl.parentNode) {
mainEl.parentNode.insertBefore(wrapper, mainEl);
wrapper.appendChild(mainEl);
console.log("[viewMode] wrapper créé, <main> encapsulé");
} else {
console.warn("[viewMode] <main> introuvable, wrapper ajouté à <body>");
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 <main> 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] <main> 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 // v5.0.0 : horloge HH:MM au milieu de la topbar. Mise à jour toutes les 30s