diff --git a/app/templates/_draco_bar.html b/app/templates/_draco_bar.html
index da47292..5f181e6 100644
--- a/app/templates/_draco_bar.html
+++ b/app/templates/_draco_bar.html
@@ -1,5 +1,5 @@
{# Barre d'apps commune dracodev — Fusion ACC-1.1 (issue #17).
- Snippet FIGÉ vendangé depuis Projets/dracodev-commons v0.1.4
+ Snippet FIGÉ vendangé depuis Projets/dracodev-commons v0.1.6
(ui/barre-apps/) : ne pas modifier la structure ici ; toute évolution
passe par une nouvelle version de commons.
CSS/JS : /static/css/draco-barre.css, /static/js/vendor/draco-barre.js.
@@ -56,8 +56,8 @@
✦ Spark
0
- •••
- {{ user.full_name[:1].upper() if user and user.get('full_name') else 'F' }}
+ •••
+ {{ user.full_name[:1].upper() if user and user.get('full_name') else 'F' }}
✦ dracodev — afficher l'en-tête
@@ -87,7 +87,13 @@
return;
}
if (act === "palette") { dire("Recherche fédérée ⌘K : arrive avec la fédération (programme Fusion)."); return; }
- if (act === "menu") { dire("Menu commun : arrive avec la fédération (programme Fusion)."); return; }
+ /* v0.1.6 : le toast placeholder "fédération" est remplacé
+ par le menu par défaut du nouveau JS — retourner false le
+ demande explicitement (un onAction existant garde sinon la
+ main). Contenu : Console agent + applications, URL résolues
+ donc dérivées -dev sur un hôte -dev. Nonce CSP du script
+ inchangé. */
+ if (act === "menu") { return false; }
if (act === "spark") {
if (window.DRACO_PANNEAU) { window.DRACO_PANNEAU.toggle(); }
else { dire("Spark : le panneau n'a pas pu se charger."); }
diff --git a/static/css/draco-barre.css b/static/css/draco-barre.css
index 2aaa71f..f6c6acb 100644
--- a/static/css/draco-barre.css
+++ b/static/css/draco-barre.css
@@ -2,7 +2,9 @@
v0.1.2 : palier mobile resserré selon la spec (voir @media 700px) ;
v0.1.3 : la marque ✦ (plus grande) ouvre le menu des applications —
c'est LE changement d'app en mobile (choix de Bruno, 2026-10-21) ;
- v0.1.4 : palier mobile élargi de 700 à 900 px (zone morte comblée).
+ v0.1.4 : palier mobile élargi de 700 à 900 px (zone morte comblée) ;
+ v0.1.6 : menus « ••• » et compte (construits par le JS, mêmes jetons
+ que le menu des applications) ; l'avatar est un vrai .
Jetons repris du prototype validé Console v7.2.
Thème : clair par défaut, sombre via [data-theme="dark"] sur
ou prefers-color-scheme si l'app intégratrice ne force rien. */
@@ -42,6 +44,29 @@
box-shadow: 0 16px 44px rgb(0 0 0 / .28);
}
.draco-apps-menu[hidden] { display: none; }
+
+/* Menus « ••• » et compte (v0.1.6) — même boîte que le menu des apps,
+ * ancrés à droite ; leurs items réutilisent .draco-apps-item. */
+.draco-more-menu, .draco-compte-menu {
+ position: absolute; top: calc(100% + 6px); right: 10px; z-index: 400;
+ min-width: 248px; padding: 6px;
+ background: var(--surface); border: 1px solid var(--line); border-radius: 14px;
+ box-shadow: 0 16px 44px rgb(0 0 0 / .28);
+}
+.draco-more-menu[hidden], .draco-compte-menu[hidden] { display: none; }
+.draco-menu-item {
+ display: flex; align-items: center; gap: 10px; width: 100%; box-sizing: border-box;
+ padding: 8px 10px; border: 0; border-radius: 10px;
+ background: transparent; color: var(--ink); font: inherit; text-align: left;
+ text-decoration: none; cursor: pointer;
+}
+.draco-menu-item:hover, .draco-menu-item:focus-visible { background: var(--surface-2); }
+.draco-menu-sep { height: 1px; margin: 6px 10px; background: var(--line); }
+.draco-compte-nom {
+ padding: 8px 10px 10px; font-weight: 700; font-size: 14px;
+ border-bottom: 1px solid var(--line); margin-bottom: 6px;
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
+}
.draco-apps-item {
display: flex; align-items: center; gap: 10px;
padding: 8px 10px; border-radius: 10px;
@@ -122,8 +147,10 @@
}
.draco-icon-btn:hover { background: var(--surface-2); }
.draco-avatar {
- width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center;
- background: #2f6fed; color: #fff; font-weight: 700;
+ width: 34px; height: 34px; border: 0; padding: 0; border-radius: 50%;
+ display: grid; place-items: center; flex: none;
+ background: #2f6fed; color: #fff; font: inherit; font-weight: 700;
+ cursor: pointer;
}
/* Rappel de l'en-tête masqué — onglet ancré au bord supérieur (v6) */
diff --git a/static/js/vendor/draco-barre.js b/static/js/vendor/draco-barre.js
index b5e454b..86ace8f 100644
--- a/static/js/vendor/draco-barre.js
+++ b/static/js/vendor/draco-barre.js
@@ -1,5 +1,10 @@
/* Barre d'apps commune dracodev — comportement (v0.1.0, ACC-0.2 ;
- * v0.1.3 : menu des applications ouvert par la marque ✦).
+ * v0.1.3 : menu des applications ouvert par la marque ✦ ;
+ * v0.1.5 : noms publics par configuration — aucun nom public d'un
+ * environnement particulier n'est codé en dur, voir « URL » ci-dessous ;
+ * v0.1.6 (prochaine publication — VERSION non bumpée dans cette PR,
+ * la publication est séparée) : dérivation -dev par l'hôte courant,
+ * menu « ••• » par défaut et menu compte fonctionnels, voir ci-dessous).
*
* 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
@@ -10,19 +15,158 @@
* current: "tiktak", // app courante
* domain: "pro", // domaine actif
* counts: { tiktak: 3, kanbante: 3, alertes: 3, spark: 0 },
+ * user: "Bruno", // nom affiché (v0.1.6) ;
+ * // ou { display_name: "Bruno", username: "bruno" }
+ * urls: { // bases des applications
+ * obsigate: "https://og.dracodev.net/",
+ * flowdeck: "https://flowdeck.dracodev.net/",
+ * tiktak: "https://tiktak.dracodev.net/",
+ * kanbante: "https://kanbante.dracodev.net/",
+ * agent: "https://agent.dracodev.net/",
+ * spark: "https://spark.dracodev.net/"
+ * },
* onAction: function (act, el) { … } // l'app relaie les actions
* } // qu'elle ne connaît pas
+ *
+ * URL des applications (contrat dev/prod, v0.1.5 puis v0.1.6) —
+ * résolution, dans l'ordre STRICT, pour chaque app :
+ * 1. DRACO_BAR_CONFIG.urls., injectée par l'app hôte (rendu
+ * serveur ou client) depuis SA configuration d'environnement —
+ * explicite, donc prioritaire, même sur un hôte -dev ;
+ * 2. (v0.1.6) SI l'hôte courant est un hôte -dev dracodev.net
+ * (première étiquette se terminant par -dev : tiktak-dev,
+ * kanbante-dev, og-dev, flowdeck-dev, agent-dev…), l'URL sœur
+ * dérivée avec -dev (voir DEV_URLS). C'est le correctif du bug
+ * constaté sur tiktak-dev / kanbante-dev, où les liens suivaient
+ * les noms SANS -dev faute de configuration explicite ;
+ * 3. le href déjà rendu dans le fragment HTML, c.-à-d. le jeton
+ * {{URL_}} remplacé côté serveur par l'app hôte ;
+ * 4. le défaut officiel ci-dessous (production, noms SANS suffixe).
+ * Un jeton {{URL_…}} non remplacé n'est jamais une URL : il est ignoré
+ * partout (config injectée comme href rendu) et ne peut pas être exposé.
+ * Cas spark (v0.1.6) : AUCUNE dérivation inventée — aucun hôte
+ * spark-dev connu ; sur un hôte -dev, spark retombe sur l'étape 3
+ * (href rendu) puis 4 (défaut prod), sauf config explicite (étape 1).
+ * Les clés agent et spark ne correspondent à aucun onglet de la barre :
+ * elles sont résolues et exposées via DRACO_BAR.urls pour le panneau
+ * Spark (panneau-agent), qui suit le même contrat d'injection.
* Actions émises (data-act) : rail, palette, domaine (data-dom),
- * alertes, spark, menu, rappel-entete. L'app intégratrice décide de
- * chacune (ouvrir la palette, le panneau Spark, changer de domaine…).
- * Action INTERNE (jamais relayée) : apps — la marque ✦ ouvre le menu
- * des applications, construit depuis les onglets ; c'est le moyen de
- * changer d'app en mobile, où les onglets sont repliés (v0.1.2).
+ * alertes, spark, rappel-entete. L'app intégratrice décide de chacune
+ * (ouvrir la palette, le panneau Spark, changer de domaine…).
+ * Actions INTERNES (jamais relayées comme avant) : apps — la marque ✦
+ * ouvre le menu des applications, construit depuis les onglets ;
+ * c'est le moyen de changer d'app en mobile, où les onglets sont
+ * repliés (v0.1.2). Menu « ••• » et compte : voir ci-dessous.
+ *
+ * Menu « ••• » (v0.1.6) — audit des hôtes : ObsiGate est le seul hôte
+ * connu à gérer l'action "menu" via onAction (il déclenche son propre
+ * bouton d'en-tête #header-menu-btn) ; les autres hôtes ne la gèrent
+ * pas et le bouton restait mort (relais vers un onAction absent, puis
+ * événement draco:bar sans écouteur). Comportement désormais :
+ * - sans onAction : un menu par défaut FONCTIONNEL s'ouvre — accès à
+ * la console agent (URL agent résolue, donc dérivée -dev sur un
+ * hôte -dev) et aux quatre applications (URL résolues) ; fermeture
+ * par clic extérieur, Échap ou choix d'un lien ;
+ * - avec onAction : l'hôte garde la main (comportement historique,
+ * zéro régression pour ObsiGate) ; il peut demander explicitement
+ * le menu par défaut en retournant false pour l'action "menu".
+ * Menu compte (v0.1.6) — l'avatar, auparavant un mort affichant
+ * un « B » codé en dur dans le fragment, est un vrai
+ * (data-act="compte"). Son initiale vient de cfg.user ou, à défaut, de
+ * la première lettre du title {{UTILISATEUR}} remplacé ; sans nom connu,
+ * le neutre ✦ est conservé (jamais de lettre codée en dur). Le clic
+ * ouvre un petit menu affichant le nom de l'utilisateur ; les items
+ * Profil / Déconnexion sont relayés à l'hôte via onAction ou, à défaut,
+ * via l'événement draco:bar (détail { act: "profil" | "deconnexion" }),
+ * mais le menu affiche l'identité même sans gestionnaire hôte.
+ * Accessibilité : vrais , aria-haspopup / aria-expanded,
+ * role="menu" / "menuitem", fermeture par Échap et clic extérieur.
*/
(function () {
"use strict";
var cfg = window.DRACO_BAR_CONFIG || {};
+ /* Défauts OFFICIELS (production) — les seuls noms publics présents
+ * dans ce fichier, documentés comme défauts (contrat dev/prod). */
+ var DEFAULT_URLS = {
+ obsigate: "https://og.dracodev.net/",
+ flowdeck: "https://flowdeck.dracodev.net/",
+ tiktak: "https://tiktak.dracodev.net/",
+ kanbante: "https://kanbante.dracodev.net/",
+ agent: "https://agent.dracodev.net/",
+ spark: "https://spark.dracodev.net/"
+ };
+
+ /* Dérivation -dev (v0.1.6) — appliquée UNIQUEMENT quand l'hôte courant
+ * est lui-même un hôte -dev dracodev.net (voir isDevHost). spark est
+ * volontairement absent : aucun hôte spark-dev connu, aucune URL
+ * inventée ; spark retombe sur href rendu puis défaut prod. */
+ var DEV_URLS = {
+ obsigate: "https://og-dev.dracodev.net/",
+ flowdeck: "https://flowdeck-dev.dracodev.net/",
+ tiktak: "https://tiktak-dev.dracodev.net/",
+ kanbante: "https://kanbante-dev.dracodev.net/",
+ agent: "https://agent-dev.dracodev.net/"
+ };
+
+ var APP_LABELS = {
+ obsigate: "ObsiGate", flowdeck: "Flowdeck",
+ tiktak: "TikTak", kanbante: "Kanbante"
+ };
+
+ /* Vrai si hostname est un hôte -dev dracodev.net : première étiquette
+ * se terminant par -dev ET domaine dracodev.net (un suffixe -dev sur
+ * un autre domaine, ou un hôte piège du type x-dev.dracodev.net.evil,
+ * ne déclenche rien). Insensible à la casse. */
+ function isDevHost(hostname) {
+ if (!hostname) return false;
+ var h = String(hostname).toLowerCase();
+ if (h.slice(-13) !== ".dracodev.net") return false;
+ var first = h.split(".")[0];
+ return first.length > 4 && first.slice(-4) === "-dev";
+ }
+
+ /* Un jeton {{…}} non remplacé n'est pas une URL : l'ignorer. */
+ function cleanUrl(u) {
+ if (!u || typeof u !== "string") return null;
+ if (u.indexOf("{{") !== -1) return null;
+ if (!u.replace(/\s/g, "")) return null;
+ return u;
+ }
+
+ function withSlash(u) {
+ return u.charAt(u.length - 1) !== "/" ? u + "/" : u;
+ }
+
+ function resolveUrls() {
+ var host = "";
+ try { host = (window.location && window.location.hostname) || ""; } catch (e) { host = ""; }
+ var dev = isDevHost(host);
+ var urls = {};
+ Object.keys(DEFAULT_URLS).forEach(function (app) {
+ /* 1. Config explicite de l'hôte — prioritaire, même sur -dev. */
+ var u = cleanUrl(cfg.urls && cfg.urls[app]);
+ /* 2. Dérivation -dev par l'hôte courant (spark : pas de dérivée). */
+ if (!u && dev) u = DEV_URLS[app] || null;
+ /* 3. href rendu dans le fragment (jeton remplacé côté serveur). */
+ if (!u) {
+ var tab = document.querySelector('.draco-apptab[data-app="' + app + '"]');
+ var href = tab ? tab.getAttribute("href") : null;
+ u = cleanUrl(href);
+ }
+ /* 4. Défaut officiel (production). */
+ if (!u) u = DEFAULT_URLS[app];
+ urls[app] = withSlash(u);
+ });
+ return urls;
+ }
+ function applyUrls(urls) {
+ document.querySelectorAll(".draco-apptab, .draco-apps-item").forEach(function (t) {
+ if (t.dataset.app && urls[t.dataset.app]) t.setAttribute("href", urls[t.dataset.app]);
+ });
+ }
+ var resolvedUrls = resolveUrls();
+
function setCounts(counts) {
Object.keys(counts || {}).forEach(function (k) {
/* Toutes les occurrences : l'onglet ET sa ligne dans le menu. */
@@ -43,26 +187,122 @@
});
}
- /* ── Menu des applications (v0.1.3) ──────────────────────────────
- * Construit depuis les onglets présents dans la barre : mêmes
- * liens, mêmes icônes, mêmes badges. Ouvert par la marque ✦
- * (data-act="apps"), fermé par un clic extérieur, Échap, ou le
- * choix d'une application (la navigation suit son cours). */
+ /* ── Identité / avatar (v0.1.6) ───────────────────────────────────
+ * Nom : cfg.user (chaîne, ou objet display_name / name / username),
+ * sinon le title de l'avatar quand le jeton {{UTILISATEUR}} a bien été
+ * remplacé côté serveur. L'initiale en découle ; sans nom, le neutre
+ * ✦ du fragment est conservé — jamais de lettre codée en dur. */
+ function userName() {
+ var u = cfg.user;
+ if (typeof u === "string" && u.replace(/\s/g, "")) return u.trim();
+ if (u && typeof u === "object") {
+ var n = u.display_name || u.name || u.username;
+ if (n && String(n).replace(/\s/g, "")) return String(n).trim();
+ }
+ return null;
+ }
+ function titleName() {
+ var av = document.querySelector(".draco-avatar");
+ var t = av ? av.getAttribute("title") : null;
+ if (t && t.indexOf("{{") === -1 && t.replace(/\s/g, "")) return t.trim();
+ return null;
+ }
+ function displayName() { return userName() || titleName(); }
+ function syncAvatar() {
+ var av = document.querySelector(".draco-avatar");
+ if (!av) return;
+ /* Compatibilité avec un fragment vendangé antérieur (avatar
+ * sans data-act) : le rendre déclencheur du menu compte. */
+ if (!av.getAttribute("data-act")) av.setAttribute("data-act", "compte");
+ if (!av.getAttribute("aria-haspopup")) av.setAttribute("aria-haspopup", "menu");
+ var name = displayName();
+ if (name) {
+ av.textContent = name.charAt(0).toUpperCase();
+ av.setAttribute("title", name);
+ av.setAttribute("aria-label", "Compte : " + name);
+ }
+ }
+
+ /* ── Menus déroulants (apps, •••, compte) ──────────────────────────
+ * Un seul menu ouvert à la fois ; fermeture par clic extérieur,
+ * Échap, ou choix d'un item. aria-expanded tenu à jour sur chaque
+ * déclencheur. Les href des menus sont (re)synchronisés avec
+ * resolvedUrls à chaque ouverture, pour que setUrls et la dérivation
+ * -dev s'y reflètent même si le menu a été construit avant. */
var bar = document.querySelector(".draco-bar");
var brand = bar ? bar.querySelector(".draco-brand") : null;
- var menu = null;
+ var moreBtn = bar ? bar.querySelector('[data-act="menu"]') : null;
+ var avatarBtn = document.querySelector(".draco-avatar");
+ var appsMenu = null, moreMenu = null, compteMenu = null;
- function buildMenu() {
- if (!bar || !brand || menu) return;
- menu = document.createElement("div");
- menu.className = "draco-apps-menu";
- menu.hidden = true;
- menu.setAttribute("role", "menu");
- menu.setAttribute("aria-label", "Applications");
+ function setExpanded(btn, open) {
+ if (btn) btn.setAttribute("aria-expanded", open ? "true" : "false");
+ }
+ function refreshMenuHrefs(menu) {
+ if (!menu) return;
+ menu.querySelectorAll(".draco-apps-item").forEach(function (a) {
+ if (a.dataset.app && resolvedUrls[a.dataset.app]) a.setAttribute("href", resolvedUrls[a.dataset.app]);
+ });
+ }
+ function togglePopup(menu, btn, force) {
+ if (!menu) return;
+ var open = typeof force === "boolean" ? force : menu.hidden;
+ menu.hidden = !open;
+ setExpanded(btn, open);
+ }
+ function closeAllPopups() {
+ if (appsMenu) togglePopup(appsMenu, brand, false);
+ if (moreMenu) togglePopup(moreMenu, moreBtn, false);
+ if (compteMenu) togglePopup(compteMenu, avatarBtn, false);
+ }
+ function tabLabel(app) {
+ var tab = document.querySelector('.draco-apptab[data-app="' + app + '"]');
+ var fort = tab ? tab.querySelector(".draco-apptab-txt strong") : null;
+ return fort ? fort.textContent : (APP_LABELS[app] || app);
+ }
+ function tabRole(app) {
+ var tab = document.querySelector('.draco-apptab[data-app="' + app + '"]');
+ var petit = tab ? tab.querySelector(".draco-apptab-txt small") : null;
+ return petit ? petit.textContent : "";
+ }
+ function menuLink(app, nom, role) {
+ var a = document.createElement("a");
+ a.className = "draco-apps-item draco-menu-item";
+ a.href = resolvedUrls[app] || "#";
+ a.dataset.app = app;
+ a.setAttribute("role", "menuitem");
+ var txt = document.createElement("span");
+ txt.className = "draco-apps-item-txt";
+ var n = document.createElement("span");
+ n.className = "draco-apps-item-nom";
+ n.textContent = nom;
+ txt.appendChild(n);
+ if (role) {
+ var r = document.createElement("span");
+ r.className = "draco-apps-item-role";
+ r.textContent = role;
+ txt.appendChild(r);
+ }
+ a.appendChild(txt);
+ return a;
+ }
+
+ /* Menu des applications (v0.1.3) — construit depuis les onglets
+ * présents dans la barre : mêmes liens, mêmes icônes, mêmes badges.
+ * Ouvert par la marque ✦ (data-act="apps"), fermé par un clic
+ * extérieur, Échap, ou le choix d'une application (la navigation
+ * suit son cours). */
+ function buildAppsMenu() {
+ if (!bar || !brand || appsMenu) return;
+ appsMenu = document.createElement("div");
+ appsMenu.className = "draco-apps-menu";
+ appsMenu.hidden = true;
+ appsMenu.setAttribute("role", "menu");
+ appsMenu.setAttribute("aria-label", "Applications");
document.querySelectorAll(".draco-apptab").forEach(function (t) {
var a = document.createElement("a");
a.className = "draco-apps-item";
- a.href = t.getAttribute("href");
+ a.href = resolvedUrls[t.dataset.app] || t.getAttribute("href");
a.dataset.app = t.dataset.app;
a.setAttribute("role", "menuitem");
var ico = t.querySelector(".draco-apptab-ico");
@@ -84,44 +324,167 @@
a.appendChild(txt);
var badge = t.querySelector(".draco-badge");
if (badge) a.appendChild(badge.cloneNode(true));
- menu.appendChild(a);
+ appsMenu.appendChild(a);
});
- bar.appendChild(menu);
+ bar.appendChild(appsMenu);
+ }
+ function toggleAppsMenu(force) {
+ buildAppsMenu();
+ if (!appsMenu) return;
+ if (force !== false) refreshMenuHrefs(appsMenu);
+ togglePopup(appsMenu, brand, force);
}
- function toggleMenu(force) {
- buildMenu();
- if (!menu) return;
- var open = typeof force === "boolean" ? force : menu.hidden;
- menu.hidden = !open;
- if (brand) brand.setAttribute("aria-expanded", open ? "true" : "false");
+ /* Menu « ••• » par défaut (v0.1.6) — console agent + applications.
+ * Construit depuis les URL résolues (donc dérivées -dev sur un hôte
+ * -dev) ; l'hôte qui gère "menu" via onAction garde la main (voir
+ * l'en-tête) et ne voit jamais ce menu, sauf s'il retourne false. */
+ function buildMoreMenu() {
+ if (!bar || !moreBtn || moreMenu) return;
+ moreMenu = document.createElement("div");
+ moreMenu.className = "draco-more-menu";
+ moreMenu.hidden = true;
+ moreMenu.setAttribute("role", "menu");
+ moreMenu.setAttribute("aria-label", "Plus d'actions");
+ moreMenu.appendChild(menuLink("agent", "Console agent", "Agent commun"));
+ var sep = document.createElement("div");
+ sep.className = "draco-menu-sep";
+ sep.setAttribute("aria-hidden", "true");
+ moreMenu.appendChild(sep);
+ ["obsigate", "flowdeck", "tiktak", "kanbante"].forEach(function (app) {
+ moreMenu.appendChild(menuLink(app, tabLabel(app), tabRole(app)));
+ });
+ bar.appendChild(moreMenu);
+ }
+ function toggleMoreMenu(force) {
+ buildMoreMenu();
+ if (!moreMenu) return;
+ if (force !== false) refreshMenuHrefs(moreMenu);
+ togglePopup(moreMenu, moreBtn, force);
+ }
+
+ /* Menu compte (v0.1.6) — en-tête d'identité + Profil / Déconnexion
+ * relayés à l'hôte. L'en-tête est rafraîchi à chaque ouverture, pour
+ * refléter un title posé après coup par l'hôte (ex. ObsiGate, qui
+ * remplit l'avatar depuis /api/auth/me après chargement). */
+ function buildCompteMenu() {
+ if (!bar || !avatarBtn || compteMenu) return;
+ compteMenu = document.createElement("div");
+ compteMenu.className = "draco-compte-menu";
+ compteMenu.hidden = true;
+ compteMenu.setAttribute("role", "menu");
+ compteMenu.setAttribute("aria-label", "Compte");
+ var head = document.createElement("div");
+ head.className = "draco-compte-nom";
+ compteMenu.appendChild(head);
+ [["profil", "Profil"], ["deconnexion", "Déconnexion"]].forEach(function (pair) {
+ var b = document.createElement("button");
+ b.type = "button";
+ b.className = "draco-menu-item";
+ b.setAttribute("data-compte-act", pair[0]);
+ b.setAttribute("role", "menuitem");
+ b.textContent = pair[1];
+ compteMenu.appendChild(b);
+ });
+ bar.appendChild(compteMenu);
+ }
+ function refreshCompteMenu() {
+ if (!compteMenu) return;
+ var head = compteMenu.querySelector(".draco-compte-nom");
+ var name = displayName();
+ if (head) head.textContent = name || "Compte";
+ }
+ function toggleCompteMenu(force) {
+ buildCompteMenu();
+ if (!compteMenu) return;
+ if (force !== false) { syncAvatar(); refreshCompteMenu(); }
+ togglePopup(compteMenu, avatarBtn, force);
+ }
+
+ /* Relais d'une action vers l'hôte : onAction si présent, sinon
+ * événement draco:bar (contrat historique des actions relayées). */
+ function relay(act, el, ev) {
+ if (typeof cfg.onAction === "function") cfg.onAction(act, el, ev);
+ else document.dispatchEvent(new CustomEvent("draco:bar", { detail: { act: act, el: el } }));
}
document.addEventListener("click", function (ev) {
- var el = ev.target.closest("[data-act]");
- if (el && el.dataset.act === "apps") {
- ev.preventDefault();
- toggleMenu();
+ /* Items du menu compte : relais + fermeture, jamais de navigation. */
+ var compteItem = ev.target.closest ? ev.target.closest("[data-compte-act]") : null;
+ if (compteItem) {
+ relay(compteItem.getAttribute("data-compte-act"), compteItem, ev);
+ toggleCompteMenu(false);
return;
}
- if (menu && !menu.hidden && !ev.target.closest(".draco-apps-menu") && !ev.target.closest(".draco-brand")) {
- toggleMenu(false);
+ var el = ev.target.closest ? ev.target.closest("[data-act]") : null;
+ if (el && el.dataset.act === "apps") {
+ ev.preventDefault();
+ if (moreMenu) toggleMoreMenu(false);
+ if (compteMenu) toggleCompteMenu(false);
+ toggleAppsMenu();
+ return;
+ }
+ if (el && el.dataset.act === "menu") {
+ if (appsMenu) toggleAppsMenu(false);
+ if (compteMenu) toggleCompteMenu(false);
+ /* Hôte prioritaire : il garde la main, sauf s'il demande
+ * explicitement le menu par défaut en retournant false.
+ * Sans onAction, le menu par défaut s'ouvre (il ne restait
+ * auparavant qu'un événement draco:bar sans écouteur). */
+ if (typeof cfg.onAction === "function") {
+ var handled = cfg.onAction("menu", el, ev);
+ if (handled === false) toggleMoreMenu();
+ } else {
+ toggleMoreMenu();
+ }
+ return;
+ }
+ if (el && el.dataset.act === "compte") {
+ if (appsMenu) toggleAppsMenu(false);
+ if (moreMenu) toggleMoreMenu(false);
+ toggleCompteMenu();
+ return;
+ }
+ /* Clic extérieur : ferme tout menu ouvert (clic dans un menu ou
+ * sur son déclencheur déjà traité ci-dessus). */
+ var inPopup = ev.target.closest ? (ev.target.closest(".draco-apps-menu") ||
+ ev.target.closest(".draco-more-menu") || ev.target.closest(".draco-compte-menu")) : null;
+ if (!inPopup && ((appsMenu && !appsMenu.hidden) || (moreMenu && !moreMenu.hidden) || (compteMenu && !compteMenu.hidden))) {
+ closeAllPopups();
}
if (!el) return;
var act = el.dataset.act;
if (act === "domaine") setDomain(el.dataset.dom);
- if (typeof cfg.onAction === "function") cfg.onAction(act, el, ev);
- else document.dispatchEvent(new CustomEvent("draco:bar", { detail: { act: act, el: el } }));
+ relay(act, el, ev);
});
document.addEventListener("keydown", function (ev) {
- if (ev.key === "Escape") toggleMenu(false);
+ if (ev.key === "Escape") closeAllPopups();
});
- /* API publique : l'app met les compteurs à jour sans recharger. */
- window.DRACO_BAR = { setCounts: setCounts, setCurrent: setCurrent, setDomain: setDomain };
+ /* API publique : l'app met les compteurs à jour sans recharger.
+ * setUrls(urls) fusionne de nouvelles bases (même contrat que
+ * DRACO_BAR_CONFIG.urls — donc prioritaires sur la dérivation -dev)
+ * et les ré-applique aux onglets et aux menus ; DRACO_BAR.urls expose
+ * les bases résolues (défauts inclus), pour le panneau Spark et tout
+ * autre consommateur de l'hôte. */
+ window.DRACO_BAR = {
+ setCounts: setCounts, setCurrent: setCurrent, setDomain: setDomain,
+ urls: resolvedUrls,
+ setUrls: function (urls) {
+ cfg.urls = Object.assign({}, cfg.urls, urls || {});
+ resolvedUrls = resolveUrls();
+ window.DRACO_BAR.urls = resolvedUrls;
+ applyUrls(resolvedUrls);
+ }
+ };
- buildMenu();
+ applyUrls(resolvedUrls);
+ buildAppsMenu();
+ buildMoreMenu();
+ buildCompteMenu();
+ syncAvatar();
+ refreshCompteMenu();
if (cfg.current) setCurrent(cfg.current);
if (cfg.domain) setDomain(cfg.domain);
if (cfg.counts) setCounts(cfg.counts);