Compare commits
38
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55351bc997 | ||
|
|
33e4d14b81 | ||
|
|
7901c7919d | ||
|
|
3b8402374a | ||
|
|
6635defb2e | ||
|
|
7fb073d462 | ||
|
|
2e59f8786b | ||
|
|
ba6069962a | ||
|
|
375de59e28 | ||
|
|
3b84a86b8f | ||
|
|
a5dac2349d | ||
|
|
da6de52198 | ||
|
|
87016e5581 | ||
|
|
e0e45ac048 | ||
|
|
1b3ba04b5b | ||
|
|
5c5180336b | ||
|
|
73bf863e40 | ||
|
|
06d0e354fe | ||
|
|
a01fba6602 | ||
|
|
54ebf2365c | ||
|
|
24eaa7f9df | ||
|
|
8e3eb3a9d7 | ||
|
|
01ce8b8ec3 | ||
|
|
5dd6809ef0 | ||
|
|
7b8bc52650 | ||
|
|
1758c3b6d2 | ||
|
|
6669633902 | ||
|
|
a65d282c0b | ||
|
|
524b43cc40 | ||
|
|
4e0b27c4d7 | ||
|
|
c6d7acfd99 | ||
|
|
bf941c71ec | ||
|
|
dcec510451 | ||
|
|
cb468b0f3c | ||
|
|
6713510ce9 | ||
|
|
bf5b32de16 | ||
|
|
27e7664ef0 | ||
|
|
ccbf1d3269 |
+21
-1
@@ -6,7 +6,7 @@ Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/),
|
|||||||
et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
> **En cours de développement** : les changements à venir sont listés dans la section
|
> **En cours de développement** : les changements à venir sont listés dans la section
|
||||||
> [Unreleased](#unreleased). La dernière version livrée est **2.81.0**.
|
> [Unreleased](#unreleased). La dernière version livrée est **2.81.3**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -14,6 +14,26 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [2.81.3] — 2026-10-28
|
||||||
|
|
||||||
|
### Modifié
|
||||||
|
|
||||||
|
- **Barre d'apps** — vendange de `dracodev-commons` v0.1.6 : dérivation `-dev` par l'hôte courant, menu « ••• » par défaut, menu compte fonctionnel, spark sans dérivation inventée.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [2.81.2] — 2026-10-28
|
||||||
|
|
||||||
|
### Modifié
|
||||||
|
|
||||||
|
- **Panneau Spark / console (PR #35)** — style console du panneau, statut « Connecté » intégré au sous-titre de l'en-tête (barre séparée supprimée), texte sous la zone de saisie supprimé, composer identique à la console (pilule, pièces jointes 4 max, textarea auto-extensible, micro dictée, envoi rond bleu), resynchronisation du domaine à chaque ouverture / changement de stockage / retour console, URL console avec `?domain=` ; la console honore `?domain=` valide (persisté avant chargement des discussions, PR agent-commun #7).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [2.81.1] — 2026-10-28
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [2.81.0] — 2026-10-28
|
## [2.81.0] — 2026-10-28
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+3
-3
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.
|
**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.
|
||||||
|
|
||||||
[]()
|
[]()
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://www.docker.com/)
|
[](https://www.docker.com/)
|
||||||
[](https://www.python.org/)
|
[](https://www.python.org/)
|
||||||
@@ -977,8 +977,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l
|
|||||||
|
|
||||||
## 📝 Changelog
|
## 📝 Changelog
|
||||||
|
|
||||||
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.81.0).
|
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.81.1).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Projet : ObsiGate | Version : 2.81.0 | Dernière mise à jour : Septembre 2026*
|
*Projet : ObsiGate | Version : 2.81.1 | Dernière mise à jour : Septembre 2026*
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
**Ultra-light web gateway for your Obsidian vaults** — Access, browse, and search all your Obsidian notes from any device via a modern, responsive web interface.
|
**Ultra-light web gateway for your Obsidian vaults** — Access, browse, and search all your Obsidian notes from any device via a modern, responsive web interface.
|
||||||
|
|
||||||
[]()
|
[]()
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://www.docker.com/)
|
[](https://www.docker.com/)
|
||||||
[](https://www.python.org/)
|
[](https://www.python.org/)
|
||||||
@@ -1152,8 +1152,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE)
|
|||||||
|
|
||||||
## 📝 Changelog
|
## 📝 Changelog
|
||||||
|
|
||||||
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.81.0).
|
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.81.1).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Project: ObsiGate | Version: 2.81.0 | Last updated: September 2026*
|
*Project: ObsiGate | Version: 2.81.1 | Last updated: September 2026*
|
||||||
|
|||||||
Generated
+1
-1
@@ -2626,7 +2626,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "obsigate-desktop"
|
name = "obsigate-desktop"
|
||||||
version = "2.81.0"
|
version = "2.81.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "obsigate-desktop"
|
name = "obsigate-desktop"
|
||||||
version = "2.81.0"
|
version = "2.81.3"
|
||||||
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
|
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
|
||||||
authors = ["Bruno Charest"]
|
authors = ["Bruno Charest"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
|
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
|
||||||
"productName": "ObsiGate",
|
"productName": "ObsiGate",
|
||||||
"version": "2.81.0",
|
"version": "2.81.3",
|
||||||
"identifier": "com.obsigate.desktop",
|
"identifier": "com.obsigate.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../frontend",
|
"frontendDist": "../frontend",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# ObsiGate — Roadmap
|
# ObsiGate — Roadmap
|
||||||
|
|
||||||
> **Version :** 2.81.0 | **Dernière mise à jour :** 2026-10-28
|
> **Version :** 2.81.3 | **Dernière mise à jour :** 2026-10-28
|
||||||
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
|
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
|
||||||
> vers les fonctionnalités livrées.
|
> vers les fonctionnalités livrées.
|
||||||
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
|
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
|
||||||
|
|||||||
@@ -2,7 +2,11 @@
|
|||||||
v0.1.2 : palier mobile resserré selon la spec (voir @media 700px) ;
|
v0.1.2 : palier mobile resserré selon la spec (voir @media 700px) ;
|
||||||
v0.1.3 : la marque ✦ (plus grande) ouvre le menu des applications —
|
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) ;
|
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 : menu compte (construit par le JS, mêmes jetons que le
|
||||||
|
menu des applications) ; l'avatar est un vrai <button>.
|
||||||
|
Correctif fusion #70 (SANS bump) : bouton trois points et menu par
|
||||||
|
défaut supprimés ; le menu de la marque gagne « Console Spark ».
|
||||||
Jetons repris du prototype validé Console v7.2.
|
Jetons repris du prototype validé Console v7.2.
|
||||||
Thème : clair par défaut, sombre via [data-theme="dark"] sur <html>
|
Thème : clair par défaut, sombre via [data-theme="dark"] sur <html>
|
||||||
ou prefers-color-scheme si l'app intégratrice ne force rien. */
|
ou prefers-color-scheme si l'app intégratrice ne force rien. */
|
||||||
@@ -32,6 +36,7 @@
|
|||||||
/* Marque — bouton du menu des applications (v0.1.3) */
|
/* Marque — bouton du menu des applications (v0.1.3) */
|
||||||
.draco-brand { display: flex; align-items: center; gap: 7px; color: var(--ink); text-decoration: none; font-weight: 800; cursor: pointer; border-radius: 10px; padding: 4px 6px; margin: -4px -6px; }
|
.draco-brand { display: flex; align-items: center; gap: 7px; color: var(--ink); text-decoration: none; font-weight: 800; cursor: pointer; border-radius: 10px; padding: 4px 6px; margin: -4px -6px; }
|
||||||
.draco-brand:hover { background: var(--surface-2); }
|
.draco-brand:hover { background: var(--surface-2); }
|
||||||
|
.draco-brand:focus-visible { outline: 2px solid var(--agent); outline-offset: 2px; }
|
||||||
.draco-brand-mark { color: var(--agent); font-size: 24px; line-height: 1; }
|
.draco-brand-mark { color: var(--agent); font-size: 24px; line-height: 1; }
|
||||||
|
|
||||||
/* Menu des applications — construit par le JS depuis les onglets */
|
/* Menu des applications — construit par le JS depuis les onglets */
|
||||||
@@ -42,6 +47,29 @@
|
|||||||
box-shadow: 0 16px 44px rgb(0 0 0 / .28);
|
box-shadow: 0 16px 44px rgb(0 0 0 / .28);
|
||||||
}
|
}
|
||||||
.draco-apps-menu[hidden] { display: none; }
|
.draco-apps-menu[hidden] { display: none; }
|
||||||
|
|
||||||
|
/* Menu compte (v0.1.6) — même boîte que le menu des apps, ancré à
|
||||||
|
* droite ; ses items réutilisent .draco-apps-item. */
|
||||||
|
.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-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 {
|
.draco-apps-item {
|
||||||
display: flex; align-items: center; gap: 10px;
|
display: flex; align-items: center; gap: 10px;
|
||||||
padding: 8px 10px; border-radius: 10px;
|
padding: 8px 10px; border-radius: 10px;
|
||||||
@@ -122,8 +150,10 @@
|
|||||||
}
|
}
|
||||||
.draco-icon-btn:hover { background: var(--surface-2); }
|
.draco-icon-btn:hover { background: var(--surface-2); }
|
||||||
.draco-avatar {
|
.draco-avatar {
|
||||||
width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center;
|
width: 34px; height: 34px; border: 0; padding: 0; border-radius: 50%;
|
||||||
background: #2f6fed; color: #fff; font-weight: 700;
|
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) */
|
/* Rappel de l'en-tête masqué — onglet ancré au bord supérieur (v6) */
|
||||||
@@ -147,8 +177,8 @@
|
|||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
/* Mobile (spec des barres, § Mobile) : la barre se resserre sur
|
/* Mobile (spec des barres, § Mobile) : la barre se resserre sur
|
||||||
l'essentiel — marque, onglet de l'app courante, alertes, Spark,
|
l'essentiel — marque, onglet de l'app courante, alertes, Spark,
|
||||||
avatar. Le rail, la recherche, les domaines et le menu « ••• »
|
avatar. Le rail, la recherche et les domaines disparaissent :
|
||||||
disparaissent : chaque app garde ses contrôles natifs (barre
|
chaque app garde ses contrôles natifs (barre
|
||||||
basse, en-tête) — un seul hamburger visible, celui de l'app.
|
basse, en-tête) — un seul hamburger visible, celui de l'app.
|
||||||
Le changement d'app passe par le menu du ✦ (v0.1.3).
|
Le changement d'app passe par le menu du ✦ (v0.1.3).
|
||||||
(v0.1.2 — avant, les onglets entiers et les pastilles domaines
|
(v0.1.2 — avant, les onglets entiers et les pastilles domaines
|
||||||
|
|||||||
+450
-42
@@ -1,5 +1,14 @@
|
|||||||
/* Barre d'apps commune dracodev — comportement (v0.1.0, ACC-0.2 ;
|
/* 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 trois points par défaut et menu compte fonctionnels, voir ci-dessous ;
|
||||||
|
* correctif fusion #70 (SANS bump de version, publication séparée) :
|
||||||
|
* bouton trois points et son menu par défaut SUPPRIMÉS, « Console Spark »
|
||||||
|
* ajoutée au menu de la marque, fermeture/focus corrigés (voir
|
||||||
|
* « Menu des applications », « Focus » et « Écouteurs globaux »).
|
||||||
*
|
*
|
||||||
* 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
|
||||||
@@ -10,19 +19,200 @@
|
|||||||
* current: "tiktak", // app courante
|
* current: "tiktak", // app courante
|
||||||
* domain: "pro", // domaine actif
|
* domain: "pro", // domaine actif
|
||||||
* counts: { tiktak: 3, kanbante: 3, alertes: 3, spark: 0 },
|
* 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
|
* onAction: function (act, el) { … } // l'app relaie les actions
|
||||||
* } // qu'elle ne connaît pas
|
* } // 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.<app>, 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_<APP>}} 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),
|
* Actions émises (data-act) : rail, palette, domaine (data-dom),
|
||||||
* alertes, spark, menu, rappel-entete. L'app intégratrice décide de
|
* alertes, spark, rappel-entete. L'app intégratrice décide de chacune
|
||||||
* chacune (ouvrir la palette, le panneau Spark, changer de domaine…).
|
* (ouvrir la palette, le panneau Spark, changer de domaine…).
|
||||||
* Action INTERNE (jamais relayée) : apps — la marque ✦ ouvre le menu
|
* Actions INTERNES (jamais relayées comme avant) : apps — la marque ✦
|
||||||
* des applications, construit depuis les onglets ; c'est le moyen de
|
* ouvre le menu des applications, construit depuis les onglets, plus
|
||||||
* changer d'app en mobile, où les onglets sont repliés (v0.1.2).
|
* (depuis le correctif #70) une entrée « Console Spark » vers la
|
||||||
|
* console de l'agent commun (URL agent résolue, voir ci-dessous) ;
|
||||||
|
* c'est le moyen de changer d'app en mobile, où les onglets sont
|
||||||
|
* repliés (v0.1.2). Menu compte : voir ci-dessous.
|
||||||
|
* L'action « menu » n'est PLUS émise nulle part : le bouton trois points
|
||||||
|
* a été supprimé (#70) ; les branches onAction("menu") des hôtes
|
||||||
|
* (TikTak/Kanbante/Flowdeck : retour false pour le menu par défaut ;
|
||||||
|
* ObsiGate : clic #header-menu-btn) sont donc mortes et ont été
|
||||||
|
* retirées des gabarits hôtes — le menu natif d'ObsiGate
|
||||||
|
* (#header-menu-btn) lui-même n'est pas touché.
|
||||||
|
*
|
||||||
|
* Retrait du bouton trois points (correctif fusion #70) — le bouton et
|
||||||
|
* son menu par défaut (console agent + applications) sont supprimés
|
||||||
|
* du fragment, du JS et du CSS. La console agent reste joignable par
|
||||||
|
* l'entrée « Console Spark » du menu de la marque (ci-dessous) et
|
||||||
|
* par le bouton Spark (panneau), donc aucun accès n'est perdu.
|
||||||
|
* Menu des applications (v0.1.3, étendu #70) — construit depuis les
|
||||||
|
* onglets présents dans la barre, puis une entrée « Console Spark »
|
||||||
|
* (logo ✦, rôle « Agent commun ») APRÈS Kanbante, vers l'URL agent
|
||||||
|
* résolue : 1) config explicite DRACO_BAR_CONFIG.urls.agent,
|
||||||
|
* 2) dérivation -dev agent-dev sur hôte -dev, 3) défaut officiel
|
||||||
|
* prod https://agent.dracodev.net/ (il n'y a pas d'onglet agent, donc
|
||||||
|
* pas d'étape « href rendu »). AUCUN hôte spark-dev n'est inventé :
|
||||||
|
* la clé spark, elle, suit toujours son propre contrat (explicite >
|
||||||
|
* href rendu > défaut prod) et ne sert qu'au panneau Spark.
|
||||||
|
* Focus (correctif #70) — ouverture au clavier : Entrée sur la marque
|
||||||
|
* (clic natif du lien) ; Espace sur la marque (keydown dédié, car un
|
||||||
|
* lien ne s'active pas à l'Espace) : ouvre le menu et met le focus
|
||||||
|
* sur son premier item. Fermeture : Échap (focus rendu au déclencheur
|
||||||
|
* qui avait ouvert le menu : la marque pour le menu apps, l'avatar
|
||||||
|
* pour le menu compte), nouvel appui sur le déclencheur (le focus y
|
||||||
|
* est déjà), choix d'un item, ou clic extérieur — ce dernier cas ne
|
||||||
|
* déplace VOLONTAIREMENT pas le focus (l'utilisateur vient de cliquer
|
||||||
|
* ailleurs : recherche, tâche… ; lui voler le focus casserait son
|
||||||
|
* geste). aria-expanded est tenu à jour sur chaque déclencheur.
|
||||||
|
* Écouteurs globaux (correctif #70, bug tiktak-dev) — les écouteurs
|
||||||
|
* clic/Échap de la barre sont posés via EventTarget.prototype (voir
|
||||||
|
* addGlobalListener), PAS via document.addEventListener. Cause
|
||||||
|
* prouvée sur TikTak (app/static/js/app.js, navigation #89) : app.js
|
||||||
|
* remplace document/window.addEventListener par un registre
|
||||||
|
* (__tcDocWin) et chaque navigation douce appelle tcInit() puis
|
||||||
|
* __tcTeardown(), qui SUPPRIME tous les écouteurs du registre — y
|
||||||
|
* compris ceux de draco-barre.js (script defer, enregistré après le
|
||||||
|
* tcInit() initial). La barre survit à la navigation (hors .layout
|
||||||
|
* remplacé) mais ses gestionnaires sont morts : un menu ouvert avant
|
||||||
|
* la navigation reste ouvert (Échap et clic extérieur sans effet) et
|
||||||
|
* la marque, lien href="/", est interceptée par l'intercepteur SPA
|
||||||
|
* brut de TikTak (__tcDocAdd, hors registre, donc survivant) comme
|
||||||
|
* une navigation interne au lieu de basculer le menu. Les écouteurs
|
||||||
|
* posés sur le prototype ne figurent pas dans le registre et ne sont
|
||||||
|
* donc jamais supprimés par le teardown ; en complément, le gabarit
|
||||||
|
* TikTak exclut désormais .draco-bar de l'intercepteur SPA, pour que
|
||||||
|
* la marque ne déclenche plus de navigation douce. Les autres hôtes
|
||||||
|
* (SSR, rechargement complet) ne sont pas concernés par le teardown
|
||||||
|
* mais bénéficient du même code ; repli : si EventTarget.prototype
|
||||||
|
* est indisponible (mini-DOM de tests), document.addEventListener
|
||||||
|
* classique est utilisé.
|
||||||
|
* Menu compte (v0.1.6) — l'avatar, auparavant un <span> mort affichant
|
||||||
|
* un « B » codé en dur dans le fragment, est un vrai <button>
|
||||||
|
* (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 <button>, aria-haspopup / aria-expanded,
|
||||||
|
* role="menu" / "menuitem", fermeture par Échap et clic extérieur.
|
||||||
*/
|
*/
|
||||||
(function () {
|
(function () {
|
||||||
"use strict";
|
"use strict";
|
||||||
var cfg = window.DRACO_BAR_CONFIG || {};
|
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) {
|
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. */
|
||||||
@@ -43,26 +233,105 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Menu des applications (v0.1.3) ──────────────────────────────
|
/* ── Identité / avatar (v0.1.6) ───────────────────────────────────
|
||||||
* Construit depuis les onglets présents dans la barre : mêmes
|
* Nom : cfg.user (chaîne, ou objet display_name / name / username),
|
||||||
* liens, mêmes icônes, mêmes badges. Ouvert par la marque ✦
|
* sinon le title de l'avatar quand le jeton {{UTILISATEUR}} a bien été
|
||||||
* (data-act="apps"), fermé par un clic extérieur, Échap, ou le
|
* remplacé côté serveur. L'initiale en découle ; sans nom, le neutre
|
||||||
* choix d'une application (la navigation suit son cours). */
|
* ✦ 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 <span>
|
||||||
|
* 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 bar = document.querySelector(".draco-bar");
|
||||||
var brand = bar ? bar.querySelector(".draco-brand") : null;
|
var brand = bar ? bar.querySelector(".draco-brand") : null;
|
||||||
var menu = null;
|
var avatarBtn = document.querySelector(".draco-avatar");
|
||||||
|
var appsMenu = null, compteMenu = null;
|
||||||
|
/* Déclencheur du menu ouvert en dernier (marque ou avatar) : Échap
|
||||||
|
* lui rend le focus à la fermeture (correctif #70). */
|
||||||
|
var lastOpener = null;
|
||||||
|
|
||||||
function buildMenu() {
|
function setExpanded(btn, open) {
|
||||||
if (!bar || !brand || menu) return;
|
if (btn) btn.setAttribute("aria-expanded", open ? "true" : "false");
|
||||||
menu = document.createElement("div");
|
}
|
||||||
menu.className = "draco-apps-menu";
|
function refreshMenuHrefs(menu) {
|
||||||
menu.hidden = true;
|
if (!menu) return;
|
||||||
menu.setAttribute("role", "menu");
|
menu.querySelectorAll(".draco-apps-item").forEach(function (a) {
|
||||||
menu.setAttribute("aria-label", "Applications");
|
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(returnFocus) {
|
||||||
|
var anyOpen = (appsMenu && !appsMenu.hidden) || (compteMenu && !compteMenu.hidden);
|
||||||
|
if (appsMenu) togglePopup(appsMenu, brand, false);
|
||||||
|
if (compteMenu) togglePopup(compteMenu, avatarBtn, false);
|
||||||
|
/* Échap seulement : le clic extérieur ne déplace pas le focus
|
||||||
|
* (voir l'en-tête, « Focus »). */
|
||||||
|
if (returnFocus && anyOpen && lastOpener && typeof lastOpener.focus === "function") lastOpener.focus();
|
||||||
|
}
|
||||||
|
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 : "";
|
||||||
|
}
|
||||||
|
/* 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) {
|
document.querySelectorAll(".draco-apptab").forEach(function (t) {
|
||||||
var a = document.createElement("a");
|
var a = document.createElement("a");
|
||||||
a.className = "draco-apps-item";
|
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.dataset.app = t.dataset.app;
|
||||||
a.setAttribute("role", "menuitem");
|
a.setAttribute("role", "menuitem");
|
||||||
var ico = t.querySelector(".draco-apptab-ico");
|
var ico = t.querySelector(".draco-apptab-ico");
|
||||||
@@ -84,44 +353,183 @@
|
|||||||
a.appendChild(txt);
|
a.appendChild(txt);
|
||||||
var badge = t.querySelector(".draco-badge");
|
var badge = t.querySelector(".draco-badge");
|
||||||
if (badge) a.appendChild(badge.cloneNode(true));
|
if (badge) a.appendChild(badge.cloneNode(true));
|
||||||
menu.appendChild(a);
|
appsMenu.appendChild(a);
|
||||||
});
|
});
|
||||||
bar.appendChild(menu);
|
/* Entrée « Console Spark » (#70) : après Kanbante, vers la console
|
||||||
|
* de l'agent commun (URL agent résolue — explicite > -dev >
|
||||||
|
* défaut prod ; aucun hôte spark-dev inventé). data-app="agent"
|
||||||
|
* l'intègre à refreshMenuHrefs/applyUrls/setCurrent comme les
|
||||||
|
* autres items ; l'icône ✦ reprend la couleur agent du CSS. */
|
||||||
|
var sep = document.createElement("div");
|
||||||
|
sep.className = "draco-menu-sep";
|
||||||
|
sep.setAttribute("aria-hidden", "true");
|
||||||
|
appsMenu.appendChild(sep);
|
||||||
|
var sparkItem = document.createElement("a");
|
||||||
|
sparkItem.className = "draco-apps-item";
|
||||||
|
sparkItem.href = resolvedUrls.agent || "#";
|
||||||
|
sparkItem.dataset.app = "agent";
|
||||||
|
sparkItem.setAttribute("role", "menuitem");
|
||||||
|
var sparkIco = document.createElement("span");
|
||||||
|
sparkIco.className = "draco-apptab-ico";
|
||||||
|
sparkIco.setAttribute("style", "--app:#c2437e");
|
||||||
|
sparkIco.textContent = "✦";
|
||||||
|
sparkItem.appendChild(sparkIco);
|
||||||
|
var sparkTxt = document.createElement("span");
|
||||||
|
sparkTxt.className = "draco-apps-item-txt";
|
||||||
|
var sparkNom = document.createElement("span");
|
||||||
|
sparkNom.className = "draco-apps-item-nom";
|
||||||
|
sparkNom.textContent = "Console Spark";
|
||||||
|
sparkTxt.appendChild(sparkNom);
|
||||||
|
var sparkRole = document.createElement("span");
|
||||||
|
sparkRole.className = "draco-apps-item-role";
|
||||||
|
sparkRole.textContent = "Agent commun";
|
||||||
|
sparkTxt.appendChild(sparkRole);
|
||||||
|
sparkItem.appendChild(sparkTxt);
|
||||||
|
appsMenu.appendChild(sparkItem);
|
||||||
|
bar.appendChild(appsMenu);
|
||||||
|
}
|
||||||
|
function toggleAppsMenu(force) {
|
||||||
|
buildAppsMenu();
|
||||||
|
if (!appsMenu) return;
|
||||||
|
if (force !== false) refreshMenuHrefs(appsMenu);
|
||||||
|
togglePopup(appsMenu, brand, force);
|
||||||
|
if (!appsMenu.hidden) lastOpener = brand;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleMenu(force) {
|
/* Menu compte (v0.1.6) — en-tête d'identité + Profil / Déconnexion
|
||||||
buildMenu();
|
* relayés à l'hôte. L'en-tête est rafraîchi à chaque ouverture, pour
|
||||||
if (!menu) return;
|
* refléter un title posé après coup par l'hôte (ex. ObsiGate, qui
|
||||||
var open = typeof force === "boolean" ? force : menu.hidden;
|
* remplit l'avatar depuis /api/auth/me après chargement). */
|
||||||
menu.hidden = !open;
|
function buildCompteMenu() {
|
||||||
if (brand) brand.setAttribute("aria-expanded", open ? "true" : "false");
|
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);
|
||||||
|
if (!compteMenu.hidden) lastOpener = avatarBtn;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("click", function (ev) {
|
/* Relais d'une action vers l'hôte : onAction si présent, sinon
|
||||||
var el = ev.target.closest("[data-act]");
|
* événement draco:bar (contrat historique des actions relayées). */
|
||||||
if (el && el.dataset.act === "apps") {
|
function relay(act, el, ev) {
|
||||||
ev.preventDefault();
|
if (typeof cfg.onAction === "function") cfg.onAction(act, el, ev);
|
||||||
toggleMenu();
|
else document.dispatchEvent(new CustomEvent("draco:bar", { detail: { act: act, el: el } }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Écouteurs globaux posés sur le prototype EventTarget quand il
|
||||||
|
* existe, pour survivre au registre/teardown d'un hôte SPA (TikTak,
|
||||||
|
* voir l'en-tête) ; sinon document.addEventListener classique. */
|
||||||
|
function addGlobalListener(type, fn) {
|
||||||
|
try {
|
||||||
|
if (typeof EventTarget !== "undefined" && EventTarget.prototype &&
|
||||||
|
EventTarget.prototype.addEventListener) {
|
||||||
|
EventTarget.prototype.addEventListener.call(document, type, fn, false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (menu && !menu.hidden && !ev.target.closest(".draco-apps-menu") && !ev.target.closest(".draco-brand")) {
|
} catch (e) { /* repli ci-dessous */ }
|
||||||
toggleMenu(false);
|
document.addEventListener(type, fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
addGlobalListener("click", function (ev) {
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
var el = ev.target.closest ? ev.target.closest("[data-act]") : null;
|
||||||
|
if (el && el.dataset.act === "apps") {
|
||||||
|
ev.preventDefault();
|
||||||
|
if (compteMenu) toggleCompteMenu(false);
|
||||||
|
toggleAppsMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (el && el.dataset.act === "compte") {
|
||||||
|
if (appsMenu) toggleAppsMenu(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-compte-menu")) : null;
|
||||||
|
if (!inPopup && ((appsMenu && !appsMenu.hidden) || (compteMenu && !compteMenu.hidden))) {
|
||||||
|
closeAllPopups(false);
|
||||||
}
|
}
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
var act = el.dataset.act;
|
var act = el.dataset.act;
|
||||||
if (act === "domaine") setDomain(el.dataset.dom);
|
if (act === "domaine") setDomain(el.dataset.dom);
|
||||||
if (typeof cfg.onAction === "function") cfg.onAction(act, el, ev);
|
relay(act, el, ev);
|
||||||
else document.dispatchEvent(new CustomEvent("draco:bar", { detail: { act: act, el: el } }));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("keydown", function (ev) {
|
addGlobalListener("keydown", function (ev) {
|
||||||
if (ev.key === "Escape") toggleMenu(false);
|
if (ev.key === "Escape") { closeAllPopups(true); return; }
|
||||||
|
/* Espace sur la marque : un lien ne s'active pas à l'Espace, donc
|
||||||
|
* ouverture explicite + focus sur le premier item du menu (#70).
|
||||||
|
* Entrée : clic natif du lien, déjà géré par le gestionnaire clic
|
||||||
|
* ci-dessus (pas de double bascule ici). */
|
||||||
|
if ((ev.key === " " || ev.key === "Spacebar") && ev.target && ev.target.closest) {
|
||||||
|
var brandEl = ev.target.closest('[data-act="apps"]');
|
||||||
|
if (brandEl) {
|
||||||
|
if (ev.preventDefault) ev.preventDefault();
|
||||||
|
toggleAppsMenu();
|
||||||
|
if (appsMenu && !appsMenu.hidden) {
|
||||||
|
var first = appsMenu.querySelector(".draco-apps-item");
|
||||||
|
if (first && typeof first.focus === "function") first.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/* API publique : l'app met les compteurs à jour sans recharger. */
|
/* API publique : l'app met les compteurs à jour sans recharger.
|
||||||
window.DRACO_BAR = { setCounts: setCounts, setCurrent: setCurrent, setDomain: setDomain };
|
* 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();
|
||||||
|
buildCompteMenu();
|
||||||
|
syncAvatar();
|
||||||
|
refreshCompteMenu();
|
||||||
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);
|
||||||
|
|||||||
+89
-17
@@ -37,16 +37,19 @@
|
|||||||
display: flex; align-items: center; gap: 9px;
|
display: flex; align-items: center; gap: 9px;
|
||||||
padding: 13px 15px; border-bottom: 1px solid var(--p-bord); background: var(--p-surface);
|
padding: 13px 15px; border-bottom: 1px solid var(--p-bord); background: var(--p-surface);
|
||||||
}
|
}
|
||||||
.draco-panneau-hd .titre { font-weight: 700; font-size: 15px; }
|
.draco-panneau-titres { min-width: 0; flex: 1; }
|
||||||
.draco-panneau-hd .soustitre { font-size: 12px; color: var(--p-doux); }
|
.draco-panneau-hd .titre { font-weight: 700; font-size: 15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.draco-panneau-hd .soustitre {
|
||||||
|
font-size: 12px; color: var(--p-doux); display: flex; align-items: center; gap: 6px;
|
||||||
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.draco-panneau-hd .soustitre .point { width: 8px; height: 8px; border-radius: 50%; background: #b9bdb2; flex: none; }
|
||||||
|
.draco-panneau-hd .soustitre.ok .point { background: #2ea36b; }
|
||||||
|
.draco-panneau-hd .soustitre.err .point { background: #cf4b32; }
|
||||||
.draco-panneau-hd .fermer {
|
.draco-panneau-hd .fermer {
|
||||||
margin-left: auto; border: 0; background: var(--p-agent); color: var(--p-texte);
|
margin-left: auto; border: 0; background: var(--p-agent); color: var(--p-texte);
|
||||||
border-radius: 8px; width: 30px; height: 30px; cursor: pointer; font-size: 14px;
|
border-radius: 8px; width: 30px; height: 30px; cursor: pointer; font-size: 14px; flex: none;
|
||||||
}
|
}
|
||||||
.draco-panneau-etat { display: flex; align-items: center; gap: 7px; font-size: 12.5px; }
|
|
||||||
.draco-panneau-etat .point { width: 9px; height: 9px; border-radius: 50%; background: #b9bdb2; flex: none; }
|
|
||||||
.draco-panneau-etat.ok .point { background: #2ea36b; }
|
|
||||||
.draco-panneau-etat.err .point { background: #cf4b32; }
|
|
||||||
/* Barre des discussions (façon Muse) */
|
/* Barre des discussions (façon Muse) */
|
||||||
.draco-disc-bar {
|
.draco-disc-bar {
|
||||||
display: flex; align-items: center; gap: 7px;
|
display: flex; align-items: center; gap: 7px;
|
||||||
@@ -112,18 +115,87 @@
|
|||||||
.draco-msg.user { align-self: flex-end; background: #2f6fed; color: #fff; border-bottom-right-radius: 4px; }
|
.draco-msg.user { align-self: flex-end; background: #2f6fed; color: #fff; border-bottom-right-radius: 4px; }
|
||||||
.draco-msg.agent { align-self: flex-start; background: var(--p-agent); border-bottom-left-radius: 4px; }
|
.draco-msg.agent { align-self: flex-start; background: var(--p-agent); border-bottom-left-radius: 4px; }
|
||||||
.draco-msg.sys { align-self: center; background: none; color: var(--p-doux); font-size: 12.5px; text-align: center; max-width: 96%; }
|
.draco-msg.sys { align-self: center; background: none; color: var(--p-doux); font-size: 12.5px; text-align: center; max-width: 96%; }
|
||||||
.draco-panneau-ft { border-top: 1px solid var(--p-bord); padding: 11px 12px; background: var(--p-surface); }
|
.draco-panneau-ft { padding: 10px 12px 11px; background: var(--p-fond); }
|
||||||
.draco-panneau-form { display: flex; gap: 8px; }
|
/* Puces de suggestions, comme la console : au-dessus du composer. */
|
||||||
.draco-panneau-form input {
|
.draco-puces {
|
||||||
flex: 1; border: 1px solid var(--p-chip-bord); border-radius: 10px; padding: 9px 11px;
|
display: flex; flex-wrap: wrap; gap: 7px; justify-content: center; padding: 0 2px 8px;
|
||||||
font: inherit; background: var(--p-champ); color: var(--p-texte);
|
|
||||||
}
|
}
|
||||||
.draco-panneau-form button {
|
.draco-puces button {
|
||||||
border: 0; border-radius: 10px; background: var(--p-accent); color: #fff;
|
border: 1px solid var(--p-chip-bord); background: var(--p-chip); color: var(--p-texte);
|
||||||
padding: 9px 14px; font: inherit; font-weight: 600; cursor: pointer;
|
border-radius: 999px; padding: 5px 12px; font: 600 12.5px system-ui; cursor: pointer;
|
||||||
|
}
|
||||||
|
.draco-puces button:hover { background: var(--p-chip-survol); }
|
||||||
|
/* ── Composer façon console (pilule) ─────────────────────────
|
||||||
|
* +, textarea auto-extensible, micro ou envoi rond bleu ↑ ;
|
||||||
|
* pendant la dictée, la rangée d'enregistrement remplace la saisie. */
|
||||||
|
.draco-composer {
|
||||||
|
display: flex; flex-direction: column; align-items: stretch; gap: 6px;
|
||||||
|
background: var(--p-surface); border: 1px solid var(--p-bord);
|
||||||
|
border-radius: 24px; padding: 8px 10px;
|
||||||
|
box-shadow: 0 1px 4px rgb(0 0 0 / .05);
|
||||||
|
}
|
||||||
|
.draco-composer [hidden] { display: none !important; }
|
||||||
|
.draco-composer-ligne { display: flex; align-items: flex-end; gap: 6px; }
|
||||||
|
.draco-composer textarea {
|
||||||
|
flex: 1; border: 0; background: none; resize: none; outline: none;
|
||||||
|
padding: 8px 2px; max-height: 152px; overflow-y: auto; line-height: 1.45;
|
||||||
|
min-height: calc(1.45em + 16px); font: inherit; color: var(--p-texte);
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
.draco-composer textarea::-webkit-scrollbar { width: 0; height: 0; display: none; }
|
||||||
|
.draco-composer .composer-btn {
|
||||||
|
width: 36px; height: 36px; border-radius: 50%; border: 0; flex: none;
|
||||||
|
background: var(--p-chip); color: var(--p-texte); font-size: 17px;
|
||||||
|
display: flex; align-items: center; justify-content: center; padding: 0; cursor: pointer;
|
||||||
|
}
|
||||||
|
.draco-composer .composer-btn:hover { background: var(--p-chip-survol); }
|
||||||
|
.draco-composer .composer-btn.danger { color: var(--p-danger); }
|
||||||
|
.draco-composer .composer-envoi {
|
||||||
|
width: 36px; height: 36px; border-radius: 50%; border: 0; flex: none;
|
||||||
|
background: #2f6fed; color: #fff; font-size: 17px; font-weight: 700;
|
||||||
|
display: flex; align-items: center; justify-content: center; padding: 0; cursor: pointer;
|
||||||
|
}
|
||||||
|
.draco-composer .composer-envoi:disabled { opacity: .5; }
|
||||||
|
.draco-composer .composer-btn .ic, .draco-composer .composer-envoi .ic { width: 19px; height: 19px; }
|
||||||
|
.draco-composer-pieces { display: flex; flex-wrap: wrap; gap: 8px; padding: 2px 2px 0; }
|
||||||
|
.draco-composer-pieces .piece-carte {
|
||||||
|
position: relative; display: flex; align-items: center; gap: 8px;
|
||||||
|
background: var(--p-chip); border: 1px solid var(--p-chip-bord);
|
||||||
|
border-radius: 12px; padding: 5px 9px 5px 5px; max-width: 230px;
|
||||||
|
}
|
||||||
|
.draco-composer-pieces .piece-carte img { width: 42px; height: 42px; object-fit: cover; border-radius: 8px; display: block; }
|
||||||
|
.draco-composer-pieces .piece-nom { font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.draco-composer-pieces .piece-doc-ext {
|
||||||
|
width: 42px; height: 42px; display: flex; align-items: center; justify-content: center;
|
||||||
|
background: var(--p-chip); border: 1px solid var(--p-chip-bord); border-radius: 8px;
|
||||||
|
}
|
||||||
|
.draco-composer-pieces .piece-ext { font-weight: 700; font-size: 10px; letter-spacing: .04em; text-transform: uppercase; opacity: .75; }
|
||||||
|
.draco-composer-pieces .piece-retirer {
|
||||||
|
position: absolute; top: -7px; right: -7px; width: 20px; height: 20px;
|
||||||
|
border-radius: 50%; border: 0; background: var(--p-texte); color: var(--p-fond);
|
||||||
|
font-size: 10px; line-height: 1; padding: 0; cursor: pointer;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.draco-composer-enreg { display: flex; align-items: center; gap: 10px; padding: 0 2px; }
|
||||||
|
.draco-composer-enreg .enreg-temps { font-variant-numeric: tabular-nums; font-weight: 700; min-width: 36px; }
|
||||||
|
.draco-composer-enreg .enreg-texte {
|
||||||
|
flex: 1; min-width: 0; color: var(--p-doux); font-size: 13px;
|
||||||
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.draco-composer-enreg .enreg-onde { display: flex; align-items: center; gap: 3px; height: 26px; flex: none; }
|
||||||
|
.draco-composer-enreg .enreg-onde span { width: 4px; height: 30%; border-radius: 2px; background: #2f6fed; }
|
||||||
|
.draco-composer.en-cours .enreg-onde span { animation: onde 1.05s ease-in-out infinite; }
|
||||||
|
.draco-composer-enreg .enreg-onde span:nth-child(3n+2) { animation-delay: .14s; }
|
||||||
|
.draco-composer-enreg .enreg-onde span:nth-child(3n) { animation-delay: .28s; }
|
||||||
|
@keyframes onde { 0%, 100% { height: 22%; } 50% { height: 96%; } }
|
||||||
|
/* Pièces jointes dans une bulle utilisateur */
|
||||||
|
.draco-msg-pieces { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 7px; }
|
||||||
|
.draco-msg-pieces img { max-width: 150px; max-height: 120px; border-radius: 10px; display: block; }
|
||||||
|
.draco-msg-pieces .piece-fichier {
|
||||||
|
display: inline-flex; align-items: center; gap: 7px;
|
||||||
|
background: var(--p-chip); border: 1px solid var(--p-chip-bord); color: var(--p-texte);
|
||||||
|
border-radius: 9px; padding: 6px 9px; font-size: 12px;
|
||||||
}
|
}
|
||||||
.draco-panneau-form button:disabled { opacity: .5; cursor: default; }
|
|
||||||
.draco-panneau-note { font-size: 11.5px; color: var(--p-faint); margin-top: 8px; text-align: center; }
|
|
||||||
.draco-missions { align-self: stretch; background: var(--p-surface); border: 1px solid var(--p-bord); border-radius: 12px; padding: 10px 12px; }
|
.draco-missions { align-self: stretch; background: var(--p-surface); border: 1px solid var(--p-bord); border-radius: 12px; padding: 10px 12px; }
|
||||||
.draco-missions h5 { font-size: 12px; text-transform: uppercase; letter-spacing: .4px; color: var(--p-doux); margin: 0 0 6px; }
|
.draco-missions h5 { font-size: 12px; text-transform: uppercase; letter-spacing: .4px; color: var(--p-doux); margin: 0 0 6px; }
|
||||||
.draco-missions label { display: block; font-size: 12px; font-weight: 600; color: var(--p-doux); margin: 8px 0 3px; }
|
.draco-missions label { display: block; font-size: 12px; font-weight: 600; color: var(--p-doux); margin: 8px 0 3px; }
|
||||||
|
|||||||
+332
-23
@@ -40,7 +40,10 @@
|
|||||||
var convKind = null; // 'principale' | 'parallele'
|
var convKind = null; // 'principale' | 'parallele'
|
||||||
var convTitre = "";
|
var convTitre = "";
|
||||||
var etatConnu = false;
|
var etatConnu = false;
|
||||||
|
var domaineCharge = null; // domaine des discussions affichées
|
||||||
|
var consoleUrlCharge = null; // URL (disc+domaine+app) de l'iframe console
|
||||||
var root = null, msgsEl = null, etatEl = null, inputEl = null, sendBtn = null;
|
var root = null, msgsEl = null, etatEl = null, inputEl = null, sendBtn = null;
|
||||||
|
var microEl = null, piecesEl = null, fichierEl = null;
|
||||||
var titreEl = null, tiroirEl = null;
|
var titreEl = null, tiroirEl = null;
|
||||||
var versionEl = null, versionConnue = "", versionMaj = 0;
|
var versionEl = null, versionConnue = "", versionMaj = 0;
|
||||||
|
|
||||||
@@ -68,11 +71,16 @@
|
|||||||
return correspondance[h] || null;
|
return correspondance[h] || null;
|
||||||
}
|
}
|
||||||
function urlConsole() {
|
function urlConsole() {
|
||||||
|
/* Le domaine de l'hôte est passé explicitement : la console
|
||||||
|
* vit sur une autre origine, son localStorage ne voit jamais
|
||||||
|
* celui de l'app hôte — sans ce paramètre, elle rouvrait
|
||||||
|
* toujours le domaine de sa propre origine, pas celui du site. */
|
||||||
var params = [];
|
var params = [];
|
||||||
var id = convId || convStockee();
|
var id = convId || convStockee();
|
||||||
if (id) params.push("disc=" + encodeURIComponent(id));
|
if (id) params.push("disc=" + encodeURIComponent(id));
|
||||||
var app = appHote();
|
var app = appHote();
|
||||||
if (app) params.push("app=" + encodeURIComponent(app));
|
if (app) params.push("app=" + encodeURIComponent(app));
|
||||||
|
params.push("domain=" + encodeURIComponent(domaine()));
|
||||||
return API + "/" + (params.length ? "?" + params.join("&") : "");
|
return API + "/" + (params.length ? "?" + params.join("&") : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,7 +155,15 @@
|
|||||||
|
|
||||||
function ouvrirConsole() {
|
function ouvrirConsole() {
|
||||||
assurerSurimpression();
|
assurerSurimpression();
|
||||||
if (!surCadre.getAttribute("src")) surCadre.src = urlConsole();
|
/* L'iframe n'est rechargée que si sa cible a changé
|
||||||
|
* (discussion, domaine ou app) : même cible = état conservé,
|
||||||
|
* cible différente = la console s'ajuste, au lieu de garder
|
||||||
|
* la discussion du premier chargement. */
|
||||||
|
var url = urlConsole();
|
||||||
|
if (!surCadre.getAttribute("src") || url !== consoleUrlCharge) {
|
||||||
|
surCadre.src = url;
|
||||||
|
consoleUrlCharge = url;
|
||||||
|
}
|
||||||
if (surOuverte) return;
|
if (surOuverte) return;
|
||||||
surOuverte = true;
|
surOuverte = true;
|
||||||
/* Naissance à la géométrie du panneau (ancrée à droite), puis
|
/* Naissance à la géométrie du panneau (ancrée à droite), puis
|
||||||
@@ -175,7 +191,12 @@
|
|||||||
setTimeout(terminer, 380); // filet si transitionend ne vient pas
|
setTimeout(terminer, 380); // filet si transitionend ne vient pas
|
||||||
surEl.style.width = largeurPanneau() + "px";
|
surEl.style.width = largeurPanneau() + "px";
|
||||||
}
|
}
|
||||||
function modeSidebarConsole() { retrecirConsole(); }
|
function modeSidebarConsole() {
|
||||||
|
retrecirConsole();
|
||||||
|
/* Retour au panneau : si le domaine a changé pendant que la
|
||||||
|
* console occupait tout l'écran, le panneau s'ajuste aussi. */
|
||||||
|
resynchroniserDomaine();
|
||||||
|
}
|
||||||
function fermerAgentConsole() { fermer(); retrecirConsole(); }
|
function fermerAgentConsole() { fermer(); retrecirConsole(); }
|
||||||
|
|
||||||
/* ── Thème : suivre l'app hôte ─────────────────────────────── */
|
/* ── Thème : suivre l'app hôte ─────────────────────────────── */
|
||||||
@@ -246,13 +267,10 @@
|
|||||||
'<div class="draco-poignee" title="Glisser pour régler la largeur — double-clic : largeur par défaut"></div>' +
|
'<div class="draco-poignee" title="Glisser pour régler la largeur — double-clic : largeur par défaut"></div>' +
|
||||||
'<div class="draco-panneau-hd">' +
|
'<div class="draco-panneau-hd">' +
|
||||||
' <span style="font-size:19px;color:#7c5cd6">✦</span>' +
|
' <span style="font-size:19px;color:#7c5cd6">✦</span>' +
|
||||||
' <div><div class="titre">Spark — agent commun</div>' +
|
' <div class="draco-panneau-titres"><div class="titre">Spark — agent commun</div>' +
|
||||||
' <div class="soustitre">Service d\'agents dracodev</div></div>' +
|
' <div class="soustitre"><span class="point"></span><span class="txt">Connexion au service…</span></div></div>' +
|
||||||
' <button type="button" class="fermer" aria-label="Fermer le panneau">✕</button>' +
|
' <button type="button" class="fermer" aria-label="Fermer le panneau">✕</button>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="draco-panneau-hd draco-panneau-etat" style="padding-top:8px;padding-bottom:8px">' +
|
|
||||||
' <span class="point"></span><span class="txt">Connexion au service…</span>' +
|
|
||||||
'</div>' +
|
|
||||||
'<div class="draco-disc-bar">' +
|
'<div class="draco-disc-bar">' +
|
||||||
' <span class="d-titre">Discussion principale</span>' +
|
' <span class="d-titre">Discussion principale</span>' +
|
||||||
' <button type="button" class="d-btn" data-disc="liste">☰ Discussions</button>' +
|
' <button type="button" class="d-btn" data-disc="liste">☰ Discussions</button>' +
|
||||||
@@ -266,17 +284,40 @@
|
|||||||
' <button type="button" data-outil="missions">🚀 Missions</button>' +
|
' <button type="button" data-outil="missions">🚀 Missions</button>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="draco-panneau-msgs"></div>' +
|
'<div class="draco-panneau-msgs"></div>' +
|
||||||
'<div class="draco-panneau-ft"><form class="draco-panneau-form">' +
|
'<div class="draco-panneau-ft">' +
|
||||||
' <input type="text" placeholder="Écrire à Spark…" autocomplete="off" aria-label="Message à Spark">' +
|
' <div class="draco-puces">' +
|
||||||
' <button type="submit">Envoyer</button></form>' +
|
' <button type="button" data-sugg="resume">Résumer ma journée</button>' +
|
||||||
' <div class="draco-panneau-note">Conversation réelle, sous-agents (J3) et missions longues (J5) vers les exécuteurs configurés ; toute écriture dans les apps passe par une approbation.</div>' +
|
' <button type="button" data-sugg="projets">Où en sont mes projets ?</button>' +
|
||||||
|
' <button type="button" data-sugg="chercher">Chercher partout…</button>' +
|
||||||
|
' </div>' +
|
||||||
|
' <form class="draco-panneau-form draco-composer">' +
|
||||||
|
' <div class="draco-composer-pieces" hidden></div>' +
|
||||||
|
' <div class="draco-composer-ligne">' +
|
||||||
|
' <button type="button" class="composer-btn" data-comp="plus" title="Joindre une image ou un document" aria-label="Joindre une image ou un document"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14"/><path d="M5 12h14"/></svg></button>' +
|
||||||
|
' <textarea rows="1" placeholder="Écrire à Spark…" autocomplete="off" aria-label="Message à Spark"></textarea>' +
|
||||||
|
' <input type="file" accept="image/*,.pdf,.docx,.odt,.txt,.md,.html,.csv" multiple hidden>' +
|
||||||
|
' <button type="button" class="composer-btn" data-comp="micro" title="Dictée vocale" aria-label="Dictée vocale" hidden><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><path d="M12 19v3"/></svg></button>' +
|
||||||
|
' <button type="submit" class="composer-envoi" title="Envoyer" aria-label="Envoyer" hidden><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20V4.5"/><path d="m5.5 11 6.5-6.5 6.5 6.5"/></svg></button>' +
|
||||||
|
' </div>' +
|
||||||
|
' <div class="draco-composer-enreg" hidden>' +
|
||||||
|
' <button type="button" class="composer-btn danger" data-comp="enreg-annuler" title="Annuler la dictée" aria-label="Annuler la dictée"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M10 11v6"/><path d="M14 11v6"/></svg></button>' +
|
||||||
|
' <span class="enreg-temps">0:00</span>' +
|
||||||
|
' <div class="enreg-onde" aria-hidden="true"></div>' +
|
||||||
|
' <span class="enreg-texte"></span>' +
|
||||||
|
' <button type="button" class="composer-btn" data-comp="enreg-stop" title="Arrêter et corriger la transcription" aria-label="Arrêter la dictée"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="6.5" y="6.5" width="11" height="11" rx="2.5"/></svg></button>' +
|
||||||
|
' <button type="button" class="composer-envoi" data-comp="enreg-envoyer" title="Arrêter et envoyer" aria-label="Arrêter et envoyer"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20V4.5"/><path d="m5.5 11 6.5-6.5 6.5 6.5"/></svg></button>' +
|
||||||
|
' </div>' +
|
||||||
|
' </form>' +
|
||||||
' <div class="draco-panneau-version" style="margin-top:6px;font-size:11.5px;opacity:.6" hidden></div>' +
|
' <div class="draco-panneau-version" style="margin-top:6px;font-size:11.5px;opacity:.6" hidden></div>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
document.body.appendChild(root);
|
document.body.appendChild(root);
|
||||||
msgsEl = root.querySelector(".draco-panneau-msgs");
|
msgsEl = root.querySelector(".draco-panneau-msgs");
|
||||||
etatEl = root.querySelector(".draco-panneau-etat");
|
etatEl = root.querySelector(".draco-panneau-hd .soustitre");
|
||||||
inputEl = root.querySelector("input");
|
inputEl = root.querySelector(".draco-composer textarea");
|
||||||
sendBtn = root.querySelector("button[type=submit]");
|
sendBtn = root.querySelector(".draco-composer button[type=submit]");
|
||||||
|
microEl = root.querySelector('[data-comp="micro"]');
|
||||||
|
piecesEl = root.querySelector(".draco-composer-pieces");
|
||||||
|
fichierEl = root.querySelector(".draco-composer input[type=file]");
|
||||||
titreEl = root.querySelector(".d-titre");
|
titreEl = root.querySelector(".d-titre");
|
||||||
tiroirEl = root.querySelector(".draco-discussions");
|
tiroirEl = root.querySelector(".draco-discussions");
|
||||||
versionEl = root.querySelector(".draco-panneau-version");
|
versionEl = root.querySelector(".draco-panneau-version");
|
||||||
@@ -288,6 +329,42 @@
|
|||||||
root.querySelector('[data-outil="approbations"]').addEventListener("click", chargerApprobations);
|
root.querySelector('[data-outil="approbations"]').addEventListener("click", chargerApprobations);
|
||||||
root.querySelector('[data-outil="missions"]').addEventListener("click", ouvrirMissions);
|
root.querySelector('[data-outil="missions"]').addEventListener("click", ouvrirMissions);
|
||||||
root.querySelector("form").addEventListener("submit", envoyer);
|
root.querySelector("form").addEventListener("submit", envoyer);
|
||||||
|
inputEl.addEventListener("input", majComposer);
|
||||||
|
inputEl.addEventListener("keydown", function (ev) {
|
||||||
|
if (ev.key === "Enter" && !ev.shiftKey) { ev.preventDefault(); envoyerComposer(); }
|
||||||
|
});
|
||||||
|
root.querySelector('[data-comp="plus"]').addEventListener("click", function () { fichierEl.click(); });
|
||||||
|
fichierEl.addEventListener("change", function (ev) {
|
||||||
|
ajouterFichiers(ev.target.files);
|
||||||
|
ev.target.value = "";
|
||||||
|
});
|
||||||
|
microEl.addEventListener("click", demarrerDictee);
|
||||||
|
root.querySelector('[data-comp="enreg-stop"]').addEventListener("click", function () { arreterDictee("stop"); });
|
||||||
|
root.querySelector('[data-comp="enreg-annuler"]').addEventListener("click", function () { arreterDictee("annuler"); });
|
||||||
|
root.querySelector('[data-comp="enreg-envoyer"]').addEventListener("click", function () { arreterDictee("envoyer"); });
|
||||||
|
Array.prototype.forEach.call(root.querySelectorAll("[data-sugg]"), function (b) {
|
||||||
|
b.addEventListener("click", function () {
|
||||||
|
var mode = b.getAttribute("data-sugg");
|
||||||
|
if (mode === "resume") {
|
||||||
|
envoyerMessage("Résume ma journée : mes tâches du jour, les cartes en Révision et l'activité récente.", []);
|
||||||
|
} else if (mode === "projets") {
|
||||||
|
envoyerMessage("Où en sont mes projets ? Fais le point sur l'état des projets et du board.", []);
|
||||||
|
} else {
|
||||||
|
inputEl.value = "Cherche « » partout : ";
|
||||||
|
majComposer();
|
||||||
|
inputEl.focus();
|
||||||
|
inputEl.setSelectionRange(9, 9);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
/* Domaine changé dans un autre onglet pendant que le panneau
|
||||||
|
* existe : l'événement storage ne part que vers les autres
|
||||||
|
* documents, donc ici il arrive bien. Le changement dans le
|
||||||
|
* même onglet est rattrapé à chaque ouverture (ouvrir()). */
|
||||||
|
window.addEventListener("storage", function (ev) {
|
||||||
|
if (ev.key === "draco_domain") resynchroniserDomaine();
|
||||||
|
});
|
||||||
|
majComposer();
|
||||||
brancherPoignee(root.querySelector(".draco-poignee"));
|
brancherPoignee(root.querySelector(".draco-poignee"));
|
||||||
appliquerTheme();
|
appliquerTheme();
|
||||||
surveillerTheme();
|
surveillerTheme();
|
||||||
@@ -326,8 +403,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function majEtat(cls, texte) {
|
function majEtat(cls, texte) {
|
||||||
etatEl.className = "draco-panneau-hd draco-panneau-etat " + cls;
|
/* Statut intégré au titre (sous-titre de l'en-tête) : plus de
|
||||||
etatEl.querySelector(".txt").textContent = texte;
|
* barre d'état séparée. Le point vert/rouge/gris reste lisible
|
||||||
|
* sans prendre une rangée entière. */
|
||||||
|
if (!etatEl) return;
|
||||||
|
etatEl.className = "soustitre " + cls;
|
||||||
|
var t = etatEl.querySelector(".txt");
|
||||||
|
if (t) t.textContent = texte;
|
||||||
}
|
}
|
||||||
function majTitre() {
|
function majTitre() {
|
||||||
titreEl.textContent = convTitre || "Discussion principale";
|
titreEl.textContent = convTitre || "Discussion principale";
|
||||||
@@ -400,9 +482,21 @@
|
|||||||
.then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); });
|
.then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Recharge la discussion du domaine courant quand il a changé
|
||||||
|
* depuis le dernier chargement (ouverture du panneau, autre
|
||||||
|
* onglet, retour du mode console). Sans changement : rien —
|
||||||
|
* on ne casse jamais une conversation en cours pour rien. */
|
||||||
|
function resynchroniserDomaine() {
|
||||||
|
if (!root || domaine() === domaineCharge) return;
|
||||||
|
msgsEl.innerHTML = "";
|
||||||
|
tiroirEl.hidden = true;
|
||||||
|
demarrerDiscussions();
|
||||||
|
}
|
||||||
|
|
||||||
function demarrerDiscussions() {
|
function demarrerDiscussions() {
|
||||||
chargerListeDiscussions()
|
chargerListeDiscussions()
|
||||||
.then(function (d) {
|
.then(function (d) {
|
||||||
|
domaineCharge = domaine();
|
||||||
var precedente = convStockee();
|
var precedente = convStockee();
|
||||||
var candidates = (d.paralleles || []).concat(d.principale ? [d.principale] : []);
|
var candidates = (d.paralleles || []).concat(d.principale ? [d.principale] : []);
|
||||||
var trouvee = candidates.filter(function (c) { return c.id === precedente; })[0];
|
var trouvee = candidates.filter(function (c) { return c.id === precedente; })[0];
|
||||||
@@ -413,6 +507,7 @@
|
|||||||
chargerHistorique();
|
chargerHistorique();
|
||||||
})
|
})
|
||||||
.catch(function () {
|
.catch(function () {
|
||||||
|
domaineCharge = null; // échec : la prochaine ouverture réessaiera
|
||||||
ajouterMsg("sys", "Discussions : service injoignable pour le moment.");
|
ajouterMsg("sys", "Discussions : service injoignable pour le moment.");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -780,19 +875,232 @@
|
|||||||
.catch(function (e) { ajouterMsg("sys", "Lancement refusé : " + e.message); });
|
.catch(function (e) { ajouterMsg("sys", "Lancement refusé : " + e.message); });
|
||||||
}
|
}
|
||||||
|
|
||||||
function envoyer(ev) {
|
/* ── Composer façon console ────────────────────────────────
|
||||||
ev.preventDefault();
|
* Mêmes règles que la console : + (images/documents, 4 max),
|
||||||
var texte = inputEl.value.trim();
|
* textarea auto-extensible, micro affiché seulement sans texte
|
||||||
if (!texte) return;
|
* ni pièce et si le navigateur le supporte, envoi rond bleu
|
||||||
|
* affiché seulement avec texte ou pièce. */
|
||||||
|
var piecesEnAttente = [];
|
||||||
|
var TYPES_IMAGES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
||||||
|
var TYPES_DOCUMENTS = ["application/pdf",
|
||||||
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
"application/vnd.oasis.opendocument.text",
|
||||||
|
"text/plain", "text/markdown", "text/html", "text/csv"];
|
||||||
|
var TAILLE_MAX_IMAGE = 5 * 1024 * 1024;
|
||||||
|
var TAILLE_MAX_DOC = 10 * 1024 * 1024;
|
||||||
|
|
||||||
|
function majComposer() {
|
||||||
|
if (!inputEl) return;
|
||||||
|
var aTexte = !!(inputEl.value && inputEl.value.trim());
|
||||||
|
sendBtn.hidden = !(aTexte || piecesEnAttente.length);
|
||||||
|
if (microEl) microEl.hidden = !dicteeSupportee || aTexte || piecesEnAttente.length;
|
||||||
|
inputEl.style.height = "auto";
|
||||||
|
var sh = inputEl.scrollHeight;
|
||||||
|
inputEl.style.height = sh ? Math.min(sh, 152) + "px" : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function rendrePiecesEnAttente() {
|
||||||
|
piecesEl.innerHTML = "";
|
||||||
|
piecesEl.hidden = !piecesEnAttente.length;
|
||||||
|
piecesEnAttente.forEach(function (p, i) {
|
||||||
|
var carte = document.createElement("div");
|
||||||
|
carte.className = "piece-carte";
|
||||||
|
var apercu;
|
||||||
|
if (TYPES_IMAGES.indexOf(p.type) >= 0) {
|
||||||
|
apercu = document.createElement("img");
|
||||||
|
apercu.src = p.dataUrl; apercu.alt = p.nom;
|
||||||
|
} else {
|
||||||
|
apercu = document.createElement("span");
|
||||||
|
apercu.className = "piece-doc-ext";
|
||||||
|
var ext = document.createElement("span");
|
||||||
|
ext.className = "piece-ext";
|
||||||
|
ext.textContent = (p.nom || "").split(".").pop() || "doc";
|
||||||
|
apercu.appendChild(ext);
|
||||||
|
}
|
||||||
|
var nom = document.createElement("span");
|
||||||
|
nom.className = "piece-nom"; nom.textContent = p.nom; nom.title = p.nom;
|
||||||
|
var x = document.createElement("button");
|
||||||
|
x.type = "button"; x.className = "piece-retirer"; x.textContent = "✕";
|
||||||
|
x.title = "Retirer " + p.nom;
|
||||||
|
x.setAttribute("aria-label", "Retirer " + p.nom);
|
||||||
|
x.addEventListener("click", function () {
|
||||||
|
piecesEnAttente.splice(i, 1);
|
||||||
|
rendrePiecesEnAttente(); majComposer();
|
||||||
|
});
|
||||||
|
carte.appendChild(apercu); carte.appendChild(nom); carte.appendChild(x);
|
||||||
|
piecesEl.appendChild(carte);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function ajouterFichiers(fichiers) {
|
||||||
|
Array.prototype.forEach.call(fichiers, function (f) {
|
||||||
|
if (piecesEnAttente.length >= 4) {
|
||||||
|
ajouterMsg("sys", "Au plus 4 pièces par message.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var estImage = TYPES_IMAGES.indexOf(f.type) >= 0;
|
||||||
|
var estDoc = TYPES_DOCUMENTS.indexOf(f.type) >= 0;
|
||||||
|
if (!estImage && !estDoc) {
|
||||||
|
ajouterMsg("sys", "« " + f.name + " » refusée : images PNG, JPEG, GIF ou WebP, ou documents PDF, DOCX, ODT, TXT, MD, HTML ou CSV seulement.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (estImage && f.size > TAILLE_MAX_IMAGE) {
|
||||||
|
ajouterMsg("sys", "« " + f.name + " » refusée : 5 Mo maximum par image.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (estDoc && f.size > TAILLE_MAX_DOC) {
|
||||||
|
ajouterMsg("sys", "« " + f.name + " » refusée : 10 Mo maximum par document.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var lecteur = new FileReader();
|
||||||
|
lecteur.onload = function () {
|
||||||
|
var dataUrl = String(lecteur.result || "");
|
||||||
|
piecesEnAttente.push({
|
||||||
|
nom: f.name, type: f.type,
|
||||||
|
base64: dataUrl.split(",")[1] || "", dataUrl: dataUrl
|
||||||
|
});
|
||||||
|
rendrePiecesEnAttente(); majComposer();
|
||||||
|
};
|
||||||
|
lecteur.readAsDataURL(f);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function envoyerComposer() {
|
||||||
|
var t = inputEl.value;
|
||||||
|
var pieces = piecesEnAttente;
|
||||||
|
piecesEnAttente = [];
|
||||||
inputEl.value = "";
|
inputEl.value = "";
|
||||||
ajouterMsg("user", texte);
|
rendrePiecesEnAttente();
|
||||||
|
majComposer();
|
||||||
|
envoyerMessage(t, pieces);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Dictée vocale — Web Speech API, 100 % navigateur, comme
|
||||||
|
* la console (fr-CA, résultats intérimaires en direct). */
|
||||||
|
var RecoVoc = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||||
|
var dicteeSupportee = !!RecoVoc;
|
||||||
|
var reco = null, enregEnCours = false, enregDebut = 0, enregMinuteur = null;
|
||||||
|
var texteAvantDictee = "", transcriptionFinale = "";
|
||||||
|
var modeFinDictee = "stop", dicteeErreur = null;
|
||||||
|
|
||||||
|
function demarrerDictee() {
|
||||||
|
if (!dicteeSupportee || enregEnCours) return;
|
||||||
|
texteAvantDictee = inputEl.value;
|
||||||
|
transcriptionFinale = "";
|
||||||
|
modeFinDictee = "stop";
|
||||||
|
dicteeErreur = null;
|
||||||
|
try { reco = new RecoVoc(); } catch (e) { reco = null; return; }
|
||||||
|
reco.lang = "fr-CA";
|
||||||
|
reco.interimResults = true;
|
||||||
|
reco.continuous = true;
|
||||||
|
reco.onresult = function (ev) {
|
||||||
|
var interim = "";
|
||||||
|
for (var i = ev.resultIndex; i < ev.results.length; i++) {
|
||||||
|
var r = ev.results[i];
|
||||||
|
if (r.isFinal) transcriptionFinale += r[0].transcript;
|
||||||
|
else interim += r[0].transcript;
|
||||||
|
}
|
||||||
|
var affiche = (texteAvantDictee + " " + transcriptionFinale + interim).trim();
|
||||||
|
root.querySelector(".enreg-texte").textContent = affiche;
|
||||||
|
inputEl.value = affiche;
|
||||||
|
};
|
||||||
|
reco.onerror = function (ev) {
|
||||||
|
if (ev.error === "aborted") return;
|
||||||
|
dicteeErreur = ev.error || "erreur";
|
||||||
|
modeFinDictee = "annuler";
|
||||||
|
};
|
||||||
|
reco.onend = terminerDictee;
|
||||||
|
try { reco.start(); } catch (e) { reco = null; return; }
|
||||||
|
enregEnCours = true;
|
||||||
|
enregDebut = Date.now();
|
||||||
|
root.querySelector(".enreg-temps").textContent = "0:00";
|
||||||
|
root.querySelector(".enreg-texte").textContent = "";
|
||||||
|
var onde = root.querySelector(".enreg-onde");
|
||||||
|
if (!onde.children.length) {
|
||||||
|
for (var j = 0; j < 14; j++) onde.appendChild(document.createElement("span"));
|
||||||
|
}
|
||||||
|
root.querySelector(".draco-composer-ligne").hidden = true;
|
||||||
|
root.querySelector(".draco-composer-enreg").hidden = false;
|
||||||
|
root.querySelector(".draco-composer").classList.add("en-cours");
|
||||||
|
enregMinuteur = window.setInterval(function () {
|
||||||
|
var s = Math.floor((Date.now() - enregDebut) / 1000);
|
||||||
|
root.querySelector(".enreg-temps").textContent = Math.floor(s / 60) + ":" + ("0" + (s % 60)).slice(-2);
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
function arreterDictee(mode) {
|
||||||
|
modeFinDictee = mode;
|
||||||
|
if (reco) {
|
||||||
|
try { reco.stop(); } catch (e) { terminerDictee(); }
|
||||||
|
} else {
|
||||||
|
terminerDictee();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function terminerDictee() {
|
||||||
|
if (!enregEnCours) return;
|
||||||
|
enregEnCours = false;
|
||||||
|
if (enregMinuteur) { window.clearInterval(enregMinuteur); enregMinuteur = null; }
|
||||||
|
reco = null;
|
||||||
|
root.querySelector(".draco-composer-ligne").hidden = false;
|
||||||
|
root.querySelector(".draco-composer-enreg").hidden = true;
|
||||||
|
root.querySelector(".draco-composer").classList.remove("en-cours");
|
||||||
|
if (modeFinDictee === "annuler") {
|
||||||
|
inputEl.value = texteAvantDictee;
|
||||||
|
if (dicteeErreur) {
|
||||||
|
ajouterMsg("sys", "Dictée vocale interrompue (" + dicteeErreur + ") — micro refusé ou indisponible ?");
|
||||||
|
dicteeErreur = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
inputEl.value = (texteAvantDictee + " " + transcriptionFinale).trim();
|
||||||
|
}
|
||||||
|
var mode = modeFinDictee;
|
||||||
|
modeFinDictee = "stop";
|
||||||
|
majComposer();
|
||||||
|
if (mode === "envoyer") envoyerComposer();
|
||||||
|
else inputEl.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function envoyer(ev) {
|
||||||
|
if (ev) ev.preventDefault();
|
||||||
|
envoyerComposer();
|
||||||
|
}
|
||||||
|
|
||||||
|
function envoyerMessage(texte, pieces) {
|
||||||
|
texte = (texte || "").trim();
|
||||||
|
pieces = pieces || [];
|
||||||
|
if (!texte && !pieces.length) return;
|
||||||
|
var el = ajouterMsg("user", texte);
|
||||||
|
if (pieces.length) {
|
||||||
|
var zone = document.createElement("div");
|
||||||
|
zone.className = "draco-msg-pieces";
|
||||||
|
pieces.forEach(function (p) {
|
||||||
|
if (TYPES_IMAGES.indexOf(p.type) >= 0) {
|
||||||
|
var img = document.createElement("img");
|
||||||
|
img.src = p.dataUrl; img.alt = p.nom;
|
||||||
|
zone.appendChild(img);
|
||||||
|
} else {
|
||||||
|
var past = document.createElement("span");
|
||||||
|
past.className = "piece-fichier";
|
||||||
|
past.textContent = p.nom;
|
||||||
|
zone.appendChild(past);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
el.appendChild(zone);
|
||||||
|
}
|
||||||
sendBtn.disabled = true;
|
sendBtn.disabled = true;
|
||||||
var attente = ajouterMsg("agent", "Spark réfléchit…");
|
var attente = ajouterMsg("agent", "Spark réfléchit…");
|
||||||
|
var corps = { message: texte, domain: domaine(), conversation_id: convId };
|
||||||
|
if (pieces.length) {
|
||||||
|
corps.pieces = pieces.map(function (p) {
|
||||||
|
return { nom: p.nom, type: p.type, donnees_base64: p.base64 };
|
||||||
|
});
|
||||||
|
}
|
||||||
fetch(API + "/api/v1/chat", {
|
fetch(API + "/api/v1/chat", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ message: texte, domain: domaine(), conversation_id: convId })
|
body: JSON.stringify(corps)
|
||||||
})
|
})
|
||||||
.then(function (r) {
|
.then(function (r) {
|
||||||
if (r.status === 401) {
|
if (r.status === 401) {
|
||||||
@@ -831,11 +1139,12 @@
|
|||||||
attente.textContent = "Service injoignable. Vérifie que le conteneur dracodev-agent est démarré, puis réessaie.";
|
attente.textContent = "Service injoignable. Vérifie que le conteneur dracodev-agent est démarré, puis réessaie.";
|
||||||
majEtat("err", "Service d'agents injoignable");
|
majEtat("err", "Service d'agents injoignable");
|
||||||
})
|
})
|
||||||
.finally(function () { sendBtn.disabled = false; inputEl.focus(); });
|
.finally(function () { sendBtn.disabled = false; majComposer(); inputEl.focus(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
function ouvrir() {
|
function ouvrir() {
|
||||||
if (!root) { construire(); demarrerDiscussions(); }
|
if (!root) { construire(); demarrerDiscussions(); }
|
||||||
|
else resynchroniserDomaine();
|
||||||
root.classList.add("open");
|
root.classList.add("open");
|
||||||
appliquerTheme();
|
appliquerTheme();
|
||||||
verifierIdentite();
|
verifierIdentite();
|
||||||
|
|||||||
+3
-4
@@ -332,7 +332,7 @@
|
|||||||
|
|
||||||
<!-- Main Application (wrapped for show/hide) -->
|
<!-- Main Application (wrapped for show/hide) -->
|
||||||
<!-- Barre d'apps commune dracodev — Fusion #215 (ACC-1.1).
|
<!-- Barre d'apps commune dracodev — Fusion #215 (ACC-1.1).
|
||||||
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
|
(ui/barre-apps/) : ne pas modifier la structure ici ; toute
|
||||||
évolution passe par une nouvelle version de commons.
|
évolution passe par une nouvelle version de commons.
|
||||||
CSS/JS : /static/draco-barre.css, /static/draco-barre.js. -->
|
CSS/JS : /static/draco-barre.css, /static/draco-barre.js. -->
|
||||||
@@ -386,8 +386,7 @@
|
|||||||
<span class="draco-spark-ico">✦</span><span class="draco-spark-txt">Spark</span>
|
<span class="draco-spark-ico">✦</span><span class="draco-spark-txt">Spark</span>
|
||||||
<span class="draco-badge" data-badge="spark" hidden>0</span>
|
<span class="draco-badge" data-badge="spark" hidden>0</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="draco-icon-btn" data-act="menu" aria-label="Plus d'actions" aria-haspopup="menu">•••</button>
|
<button type="button" class="draco-avatar" id="draco-avatar" data-act="compte" aria-label="Compte" aria-haspopup="menu" aria-expanded="false" title="">✦</button>
|
||||||
<span class="draco-avatar" id="draco-avatar" title="">✦</span>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="draco-bar-rappel" data-act="rappel-entete" hidden>✦ dracodev — afficher l'en-tête</div>
|
<div class="draco-bar-rappel" data-act="rappel-entete" hidden>✦ dracodev — afficher l'en-tête</div>
|
||||||
@@ -432,7 +431,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (act === "palette") { var s = document.getElementById("search-input"); if (s) { s.focus(); s.scrollIntoView({ block: "center" }); } return; }
|
if (act === "palette") { var s = document.getElementById("search-input"); if (s) { s.focus(); s.scrollIntoView({ block: "center" }); } return; }
|
||||||
if (act === "menu") { var m = document.getElementById("header-menu-btn"); if (m) m.click(); return; }
|
/* Correctif fusion #70 : relais "menu" supprimé avec le bouton trois points de la barre (la console agent est dans le menu de la marque, entrée « Console Spark »). Le bouton natif #header-menu-btn d'ObsiGate lui-même est inchangé et reste utilisable depuis l'en-tête ObsiGate. */
|
||||||
if (act === "alertes") { dire("Alertes unifiées : arrive avec la fédération (programme Fusion)."); return; }
|
if (act === "alertes") { dire("Alertes unifiées : arrive avec la fédération (programme Fusion)."); return; }
|
||||||
if (act === "spark") {
|
if (act === "spark") {
|
||||||
if (window.DRACO_PANNEAU) { window.DRACO_PANNEAU.toggle(); }
|
if (window.DRACO_PANNEAU) { window.DRACO_PANNEAU.toggle(); }
|
||||||
|
|||||||
@@ -59,11 +59,11 @@ function _sharedIcon(size = 16) {
|
|||||||
// renommer/déplacer/supprimer). Le menu ⋮ est un menu propre ci-dessous.
|
// renommer/déplacer/supprimer). Le menu ⋮ est un menu propre ci-dessous.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/** Icône cadenas (U+1F510) du nœud Secrets, distincte de la maison et du diamant. */
|
/** Icône coffre-fort du nœud Secrets (SVG Safe Vault Streamline, fourni par Bruno), distincte de la maison et du diamant. Le tracé utilise currentColor pour rester lisible en thème clair comme sombre (le noir d'origine serait invisible en sombre). */
|
||||||
function _secretsIcon() {
|
function _secretsIcon() {
|
||||||
const s = document.createElement("span");
|
const s = document.createElement("span");
|
||||||
s.className = "coffres-icon-lock";
|
s.className = "coffres-icon-lock";
|
||||||
s.textContent = "\u{1F510}";
|
s.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14" aria-hidden="true"><g stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"><path d="M12.5 0.5h-11c-0.552285 0 -1 0.447715 -1 1V11c0 0.5523 0.447715 1 1 1h11c0.5523 0 1 -0.4477 1 -1V1.5c0 -0.552285 -0.4477 -1 -1 -1Z"/><path d="M8.5 8c0.9665 0 1.75 -0.7835 1.75 -1.75S9.4665 4.5 8.5 4.5s-1.75 0.7835 -1.75 1.75S7.5335 8 8.5 8Z"/><path d="M8.5 3.25V4.5"/><path d="M8.5 8v1.25"/><path d="M11.5 6.25h-1.25"/><path d="M6.75 6.25H5.5"/><path d="m10.62 4.12988 -0.88001 0.88"/><path d="m7.26 7.48999 -0.88 0.88"/><path d="m10.62 8.36999 -0.88001 -0.88"/><path d="m7.26 5.00988 -0.88 -0.88"/><path d="M3 4.5V8"/><path d="M2 12v1.5"/><path d="M11.5 12v1.5"/></g></svg>';
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -14372,7 +14372,8 @@ body.has-draco-bar .app-container { height: calc(100vh - 60px); }
|
|||||||
.coffres-alerts { margin: 12px 0; }
|
.coffres-alerts { margin: 12px 0; }
|
||||||
.coffres-alert { color: var(--warning, #f59e0b); padding: 2px 0; }
|
.coffres-alert { color: var(--warning, #f59e0b); padding: 2px 0; }
|
||||||
.coffres-form { display: grid; gap: 8px; max-width: 560px; margin: 8px 0; }
|
.coffres-form { display: grid; gap: 8px; max-width: 560px; margin: 8px 0; }
|
||||||
.coffres-icon-lock { font-size: 16px; line-height: 1; }
|
.coffres-icon-lock { display: inline-flex; align-items: center; line-height: 1; }
|
||||||
|
.coffres-icon-lock svg { width: 16px; height: 16px; flex: none; }
|
||||||
.coffres-alert-dot { color: var(--warning, #f59e0b); font-weight: 700; margin-left: 4px; }
|
.coffres-alert-dot { color: var(--warning, #f59e0b); font-weight: 700; margin-left: 4px; }
|
||||||
.coffres-context-menu { position: fixed; z-index: 1000; min-width: 220px; background: var(--bg-secondary, #161b22); border: 1px solid var(--border, #21262d); border-radius: 8px; padding: 4px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); }
|
.coffres-context-menu { position: fixed; z-index: 1000; min-width: 220px; background: var(--bg-secondary, #161b22); border: 1px solid var(--border, #21262d); border-radius: 8px; padding: 4px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); }
|
||||||
.coffres-context-item { display: block; width: 100%; text-align: left; padding: 8px 12px; background: transparent; border: none; color: var(--text-primary, #e6edf3); cursor: pointer; border-radius: 6px; }
|
.coffres-context-item { display: block; width: 100%; text-align: left; padding: 8px 12px; background: transparent; border: none; color: var(--text-primary, #e6edf3); cursor: pointer; border-radius: 6px; }
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsigate",
|
"name": "obsigate",
|
||||||
"version": "2.81.0",
|
"version": "2.81.3",
|
||||||
"description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.",
|
"description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.",
|
||||||
"main": "patch.js",
|
"main": "patch.js",
|
||||||
"directories": {
|
"directories": {
|
||||||
|
|||||||
Reference in New Issue
Block a user