Merge pull request 'Kanbante 0.46.1 — HUB-18 : thème commun, barre v0.5.0 + initialisation thème Hub (fusion #132)' (#85) from hub18-theme-commun into main
CI / test (push) Failing after 1s

This commit was merged in pull request #85.
This commit is contained in:
2026-11-06 09:50:49 -05:00
6 changed files with 221 additions and 2 deletions
+10
View File
@@ -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 <hub>/api/profil/theme`, cookie draco ; `<hub>` = 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.46.0 — 2026-11-06 — Correspondance draco par utilisateur (HUB-16, carte fusion #130) ## 0.46.0 — 2026-11-06 — Correspondance draco par utilisateur (HUB-16, carte fusion #130)
Version **mineure** : c'est une fonction (nouveau comportement d'identité), selon le patron du dépôt (fonctions en mineur — 0.24.0 comptes, 0.25.0 jetons ; les 0.45.x étaient des vendanges et correctifs). Version **mineure** : c'est une fonction (nouveau comportement d'identité), selon le patron du dépôt (fonctions en mineur — 0.24.0 comptes, 0.25.0 jetons ; les 0.45.x étaient des vendanges et correctifs).
+1 -1
View File
@@ -1 +1 @@
0.46.0 0.46.1
+135
View File
@@ -36,6 +36,12 @@
* remplissage), aligné à gauche du libellé via .draco-menu-icone. * 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 * 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. * 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 <hub>/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 * 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 * (patron validé par le prototype — les onclick du HTML généré sont
@@ -268,6 +274,131 @@
} }
var resolvedUrls = resolveUrls(); 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 <hub>/api/profil/theme → {"theme": "sombre"|"clair"|"systeme"|null}
* authentifié par le cookie draco (credentials: "include").
* <hub> 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) { function setCounts(counts) {
Object.keys(counts || {}).forEach(function (k) { Object.keys(counts || {}).forEach(function (k) {
/* Toutes les occurrences : l'onglet ET sa ligne dans le menu. */ /* Toutes les occurrences : l'onglet ET sa ligne dans le menu. */
@@ -695,6 +826,9 @@
window.DRACO_BAR = { window.DRACO_BAR = {
setCounts: setCounts, setCurrent: setCurrent, setDomain: setDomain, setCounts: setCounts, setCurrent: setCurrent, setDomain: setDomain,
urls: resolvedUrls, 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) { setUrls: function (urls) {
cfg.urls = Object.assign({}, cfg.urls, urls || {}); cfg.urls = Object.assign({}, cfg.urls, urls || {});
resolvedUrls = resolveUrls(); resolvedUrls = resolveUrls();
@@ -709,6 +843,7 @@
buildCompteMenu(); buildCompteMenu();
syncAvatar(); syncAvatar();
refreshCompteMenu(); refreshCompteMenu();
initThemeCommun();
if (cfg.current) setCurrent(cfg.current); if (cfg.current) setCurrent(cfg.current);
if (cfg.domain) setDomain(cfg.domain); if (cfg.domain) setDomain(cfg.domain);
if (cfg.counts) setCounts(cfg.counts); if (cfg.counts) setCounts(cfg.counts);
+24
View File
@@ -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 : /* 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 replier un tiroir de 264 px n'a pas de sens, et avant ce
correctif il basculait l'état desktop (tiroir 68 px). */ correctif il basculait l'état desktop (tiroir 68 px). */
+5 -1
View File
@@ -30,8 +30,12 @@ def _sha256(chemin):
def test_barre_css_js_verbatim_commons_v041(): 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/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(): def test_js_icones_espace_de_noms_svg():
+46
View File
@@ -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 <hub>/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