From 20c5c38a19cb334ea5dcd0f263c60a1a8b9bdbd9 Mon Sep 17 00:00:00 2001 From: Spark Date: Fri, 6 Nov 2026 14:30:23 +0000 Subject: [PATCH] =?UTF-8?q?Kanbante=200.46.1=20=E2=80=94=20HUB-18=20:=20th?= =?UTF-8?q?=C3=A8me=20commun,=20barre=20v0.5.0=20+=20initialisation=20th?= =?UTF-8?q?=C3=A8me=20Hub=20dans=20nav.js=20(fusion=20#132)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 10 +++ VERSION | 2 +- static/js/draco-barre.js | 135 +++++++++++++++++++++++++++++++++++++++ static/js/nav.js | 24 +++++++ tests/test_barre_v041.py | 6 +- tests/test_barre_v050.py | 46 +++++++++++++ 6 files changed, 221 insertions(+), 2 deletions(-) create mode 100644 tests/test_barre_v050.py diff --git a/CHANGELOG.md b/CHANGELOG.md index def63f6..0b4f195 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## 0.46.1 — 2026-11-06 — Barre commune v0.5.0 : thème commun (HUB-18, fusion #132) + +> **Hypothèse d'ordre** : cette version suppose la fusion préalable de la PR #84 (Kanbante 0.46.0, HUB-16) ; branchée sur `main` (0.45.12), elle vise **0.46.1**. + +- **Barre d'apps v0.5.0 vendangée (dracodev-commons, branche `hub18-theme-commun`, PR commons #12)** : `static/js/draco-barre.js` remplacé **verbatim** — SHA-256 JS `384e2b02…6e188ce11c7` (preuve dans `tests/test_barre_v050.py`). Le CSS est inchangé en v0.5.0 (empreinte v0.4.1 conservée ; `tests/test_barre_v041.py` supersédé pour le seul JS). +- **Thème commun** : la barre apprend la préférence de thème du profil du Hub (`GET /api/profil/theme`, cookie draco ; `` = URL résolue de la barre, jamais figée) et l'applique à l'ouverture : cache `localStorage` `draco-theme-hub` (TTL 12 h) appliqué synchrone, puis fetch asynchrone non bloquant ; `sombre`/`clair` posent `data-theme`, `systeme` le retire (`prefers-color-scheme` décide). **Repli intégral** si le Hub ne répond pas, répond non-OK ou `theme: null` : l'amorce actuelle (choix local `kanbante-theme`, défaut sombre) reste seule maître. +- **Initialisation propre à Kanbante** (`static/js/nav.js`) : écoute de l'événement `draco:theme` de la barre (+ reprise de `DRACO_BAR.themeMode` au démarrage, la barre s'exécutant avant nav.js) — le mode effectif est posé sur `data-theme` et le bouton de bascule est resynchronisé (`syncThemeBtn`). Le choix local `kanbante-theme` n'est **pas** écrasé : le bouton reste le maître pour la session. +- **Cache** : le bump du fichier `VERSION` change le `?v=` des CSS/JS de la barre ; `APP_VERSION` suit automatiquement (source unique `VERSION`). +- Tests : `tests/test_barre_v050.py` (verbatim v0.5.0 prouvé par SHA-256, marqueurs thème, initialisation nav.js). + ## 0.45.12 — 2026-11-05 — Barre commune v0.4.1 : correctif des icônes invisibles du menu compte (fusion #125) - **Barre d'apps v0.4.1 vendangée (dracodev-commons, branche `menu-compte-icones-svg-ns-v0-4-1`, PR commons #11)** : `static/js/draco-barre.js` remplacé **verbatim** — SHA-256 JS `82c4940e…b34f85a42` (preuve dans `tests/test_barre_v041.py`). Le CSS est inchangé (empreinte v0.4.0 conservée) ; le gabarit `_draco_bar.html` est inchangé. diff --git a/VERSION b/VERSION index cbb488e..620104d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.45.12 +0.46.1 diff --git a/static/js/draco-barre.js b/static/js/draco-barre.js index 7078871..7b66b81 100644 --- a/static/js/draco-barre.js +++ b/static/js/draco-barre.js @@ -36,6 +36,12 @@ * remplissage), aligné à gauche du libellé via .draco-menu-icone. * L'en-tête d'identité ne reçoit pas d'icône. Aucun changement de * comportement : liens résolus et Déconnexion relayée inchangés. + * v0.5.0 (carte HUB-18, fusion #132) : thème commun de la suite — + * la barre apprend la préférence de thème du profil du Hub + * (GET /api/profil/theme, cookie draco) et l'applique à + * l'ouverture par le mécanisme data-theme des hôtes, avec cache + * local court et repli intégral sur le comportement actuel de + * l'hôte si le Hub ne répond pas. Voir « Thème commun » plus bas. * * Aucun gestionnaire inline : un seul écouteur délégué sur le document * (patron validé par le prototype — les onclick du HTML généré sont @@ -268,6 +274,131 @@ } var resolvedUrls = resolveUrls(); + /* ── Thème commun (v0.5.0, carte HUB-18, fusion #132) ──────────── + * La préférence de thème de la suite vit dans le profil du Hub : + * GET /api/profil/theme → {"theme": "sombre"|"clair"|"systeme"|null} + * authentifié par le cookie draco (credentials: "include"). + * est l'URL RÉSOLUE ci-dessus (explicite > dérivation -dev > + * défaut officiel) — jamais un nom figé de plus. + * Application par le mécanisme data-theme existant des hôtes : + * "sombre" → data-theme="dark", "clair" → data-theme="light", + * "systeme" → attribut RETIRÉ (prefers-color-scheme décide, garde + * html:not([data-theme]) du CSS de la barre comme des hôtes). + * L'événement draco:theme ({theme, mode} — mode = "dark"/"light" + * EFFECTIF) est ensuite dispatché pour les hôtes dont le mécanisme + * propre doit suivre (icônes et libellés, feuilles de style, + * palettes ShaarIt, attribut dédié de la console…) ; un hôte qui + * s'initialise après la barre lit la même information dans + * DRACO_BAR.themePref / DRACO_BAR.themeMode. + * Ordre d'application : 1) le cache local FRAIS est appliqué de + * façon SYNCHRONE à l'initialisation de la barre (script en defer + * chez tous les hôtes : avant DOMContentLoaded, donc avant le + * premier rendu dans le cas général) ; 2) un fetch ASYNCHRONE et + * NON BLOQUANT rafraîchit le cache et ré-applique si la préférence + * a changé. À la toute première visite (aucun cache), un bref + * flash du thème local de l'hôte reste possible le temps du fetch : + * c'est le prix du repli — l'état affiché transitoire est le thème + * que l'hôte aurait de toute façon choisi seul, jamais un état + * cassé, et les visites suivantes sont servies par le cache. + * Cache : localStorage "draco-theme-hub" ({theme, ts}), TTL 12 h + * (ordre de la journée) ; il ne contient QUE la préférence du Hub, + * jamais le choix local de l'hôte. + * REPLI INTÉGRAL : Hub injoignable, réponse non-OK (401…), JSON + * invalide, theme null ou valeur inconnue → RIEN n'est appliqué et + * RIEN n'est mis en cache ; un échec de fetch n'efface pas un cache + * existant. Le comportement actuel de l'hôte (son localStorage, + * puis le système) reste alors exactement le maître, comme avant + * la v0.5.0. Le sélecteur de thème LOCAL de chaque hôte reste + * fonctionnel : la barre n'applique qu'à l'ouverture (cache, puis + * fetch) ; elle ne ré-applique ensuite que sur changement du + * schéma système, et seulement tant que la préférence résolue est + * "systeme". */ + var THEME_CACHE_CLE = "draco-theme-hub"; + var THEME_CACHE_TTL_MS = 12 * 60 * 60 * 1000; + var themePref = null; /* "sombre"|"clair"|"systeme"|null (non résolue) */ + var themeMode = null; /* "dark"|"light"|null : mode effectif appliqué */ + + function themeSystemeEstSombre() { + try { + return !!(window.matchMedia && + window.matchMedia("(prefers-color-scheme: dark)").matches); + } catch (e) { return false; } + } + function lireCacheTheme() { + try { + var brut = window.localStorage && + window.localStorage.getItem(THEME_CACHE_CLE); + if (!brut) return null; + var c = JSON.parse(brut); + if (!c || typeof c.ts !== "number") return null; + if (Date.now() - c.ts > THEME_CACHE_TTL_MS) return null; + if (c.theme === "sombre" || c.theme === "clair" || c.theme === "systeme") + return c.theme; + } catch (e) { /* stockage indisponible ou corrompu : pas de cache */ } + return null; + } + function ecrireCacheTheme(theme) { + try { + if (window.localStorage) { + window.localStorage.setItem(THEME_CACHE_CLE, + JSON.stringify({ theme: theme, ts: Date.now() })); + } + } catch (e) { /* stockage indisponible : le prochain fetch refera le travail */ } + } + function appliquerThemeCommun(theme) { + var root = document.documentElement; + if (!root) return; + if (theme === "sombre" || theme === "clair") { + themeMode = theme === "sombre" ? "dark" : "light"; + root.setAttribute("data-theme", themeMode); + } else if (theme === "systeme") { + themeMode = themeSystemeEstSombre() ? "dark" : "light"; + root.removeAttribute("data-theme"); + } else { + return; + } + themePref = theme; + if (window.DRACO_BAR) { + window.DRACO_BAR.themePref = themePref; + window.DRACO_BAR.themeMode = themeMode; + } + try { + document.dispatchEvent(new CustomEvent("draco:theme", + { detail: { theme: themePref, mode: themeMode } })); + } catch (e) { /* aucun écouteur hôte : data-theme suffit */ } + } + function initThemeCommun() { + var enCache = lireCacheTheme(); + if (enCache) appliquerThemeCommun(enCache); + /* Suivi du schéma système tant que la préférence est "systeme" : + * sans data-theme, le CSS suit tout seul ; l'événement est + * re-dispatché pour les hôtes qui matérialisent le mode. */ + try { + var mq = window.matchMedia && + window.matchMedia("(prefers-color-scheme: dark)"); + if (mq && mq.addEventListener) { + mq.addEventListener("change", function () { + if (themePref === "systeme") appliquerThemeCommun("systeme"); + }); + } + } catch (e) { /* pas de matchMedia : pas de suivi */ } + if (typeof window.fetch !== "function") return; + var url = withSlash(resolvedUrls.hub || DEFAULT_URLS.hub) + + "api/profil/theme"; + window.fetch(url, { credentials: "include" }).then(function (resp) { + if (!resp || !resp.ok) return null; + return resp.json(); + }).then(function (data) { + if (!data) return; + var t = data.theme; + if (t === "sombre" || t === "clair" || t === "systeme") { + ecrireCacheTheme(t); + appliquerThemeCommun(t); + } + /* theme null ou inconnu : repli intégral, rien n'est touché. */ + }).catch(function () { /* Hub injoignable : repli intégral */ }); + } + function setCounts(counts) { Object.keys(counts || {}).forEach(function (k) { /* Toutes les occurrences : l'onglet ET sa ligne dans le menu. */ @@ -695,6 +826,9 @@ window.DRACO_BAR = { setCounts: setCounts, setCurrent: setCurrent, setDomain: setDomain, urls: resolvedUrls, + /* Thème commun (v0.5.0) : préférence du Hub résolue et mode + * effectif appliqué ; null tant que rien n'a été résolu (repli). */ + themePref: null, themeMode: null, setUrls: function (urls) { cfg.urls = Object.assign({}, cfg.urls, urls || {}); resolvedUrls = resolveUrls(); @@ -709,6 +843,7 @@ buildCompteMenu(); syncAvatar(); refreshCompteMenu(); + initThemeCommun(); if (cfg.current) setCurrent(cfg.current); if (cfg.domain) setDomain(cfg.domain); if (cfg.counts) setCounts(cfg.counts); diff --git a/static/js/nav.js b/static/js/nav.js index 09e55e3..9fdfee8 100644 --- a/static/js/nav.js +++ b/static/js/nav.js @@ -63,6 +63,30 @@ }); } + /* Thème commun de la suite (HUB-18, fusion #132) : la barre + commune (v0.5.0) applique la préférence du Hub à l'ouverture — + directement sur data-theme depuis son cache, et via + l'événement « draco:theme » quand le fetch aboutit. Ici, on + resynchronise le bouton de bascule avec le mode effectif. + Le choix local « kanbante-theme » n'est PAS écrasé : la + préférence du Hub initialise l'ouverture, le bouton reste le + maître pour la session (son clic mémorise comme avant). + Repli : Hub muet → aucun événement, l'amorce de _header.html + (choix local, défaut sombre) reste seule maître, comme avant. + La barre (defer) s'exécute avant nav.js : sa préférence déjà + résolue depuis son cache est aussi reprise au démarrage. */ + function appliquerThemeCommun(mode) { + document.documentElement.setAttribute('data-theme', mode); + syncThemeBtn(mode); + } + document.addEventListener('draco:theme', function (e) { + var mode = e && e.detail && e.detail.mode; + if (mode === 'dark' || mode === 'light') appliquerThemeCommun(mode); + }); + if (window.DRACO_BAR && (window.DRACO_BAR.themeMode === 'dark' || window.DRACO_BAR.themeMode === 'light')) { + appliquerThemeCommun(window.DRACO_BAR.themeMode); + } + /* En mobile, le bouton de repli dans le tiroir FERME le tiroir : replier un tiroir de 264 px n'a pas de sens, et avant ce correctif il basculait l'état desktop (tiroir 68 px). */ diff --git a/tests/test_barre_v041.py b/tests/test_barre_v041.py index 7aa9e16..6205e03 100644 --- a/tests/test_barre_v041.py +++ b/tests/test_barre_v041.py @@ -30,8 +30,12 @@ def _sha256(chemin): def test_barre_css_js_verbatim_commons_v041(): + # Remplacé en 0.46.1 : la re-vendange v0.5.0 (HUB-18, thème + # commun) supersède l'empreinte JS v0.4.1 — l'empreinte courante + # est verrouillée par tests/test_barre_v050.py. Le CSS, inchangé + # en v0.5.0, garde son empreinte v0.4.1 ; le JS a quitté la v0.4.1. assert _sha256("static/css/draco-barre.css") == SHA256_CSS_V041 - assert _sha256("static/js/draco-barre.js") == SHA256_JS_V041 + assert _sha256("static/js/draco-barre.js") != SHA256_JS_V041 def test_js_icones_espace_de_noms_svg(): diff --git a/tests/test_barre_v050.py b/tests/test_barre_v050.py new file mode 100644 index 0000000..1890187 --- /dev/null +++ b/tests/test_barre_v050.py @@ -0,0 +1,46 @@ +"""Barre commune v0.5.0 (HUB-18, fusion #132) — re-vendange 0.46.1. + +- JS vendangé VERBATIM depuis dracodev-commons, branche + hub18-theme-commun (PR commons #12), ui/barre-apps/. Le CSS est + inchangé en v0.5.0 : il garde l'empreinte v0.4.1 (verrouillée + par tests/test_barre_v041.py). +- Thème commun : la barre apprend la préférence du Hub + (GET /api/profil/theme) et l'applique à l'ouverture ; + l'initialisation propre à Kanbante vit dans static/js/nav.js + (écouteur draco:theme → data-theme + resynchronisation du + bouton de bascule, sans écraser le choix local kanbante-theme). +""" +import hashlib +from pathlib import Path + +R = Path(__file__).resolve().parents[1] + +SHA256_JS_V050 = "384e2b0248a7c84d77fbb6eae6c25366828554162556a6b5b19c96e188ce11c7" + + +def _sha256(chemin): + return hashlib.sha256((R / chemin).read_bytes()).hexdigest() + + +def test_barre_js_verbatim_commons_v050(): + assert _sha256("static/js/draco-barre.js") == SHA256_JS_V050 + + +def test_js_theme_commun_hub18(): + js = (R / "static/js/draco-barre.js").read_text(encoding="utf-8") + assert "api/profil/theme" in js + assert 'credentials: "include"' in js + assert "draco-theme-hub" in js + assert 'CustomEvent("draco:theme"' in js + assert "themePref" in js and "themeMode" in js + assert "resolvedUrls.hub" in js + # Comportements conservés (v0.4.1) : icônes en espace de noms SVG. + assert 'createElementNS(SVG_NS, "svg")' in js + + +def test_nav_js_init_theme_hub(): + nav = (R / "static/js/nav.js").read_text(encoding="utf-8") + assert 'addEventListener(\'draco:theme\'' in nav or 'addEventListener("draco:theme"' in nav + assert "appliquerThemeCommun" in nav + assert "syncThemeBtn(mode)" in nav + assert "DRACO_BAR" in nav and "themeMode" in nav