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
CI / test (push) Failing after 1s
This commit was merged in pull request #85.
This commit is contained in:
@@ -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).
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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). */
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user