Compare commits
28
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b3ba04b5b | ||
|
|
5c5180336b | ||
|
|
73bf863e40 | ||
|
|
06d0e354fe | ||
|
|
a01fba6602 | ||
|
|
54ebf2365c | ||
|
|
24eaa7f9df | ||
|
|
8e3eb3a9d7 | ||
|
|
01ce8b8ec3 | ||
|
|
5dd6809ef0 | ||
|
|
7b8bc52650 | ||
|
|
1758c3b6d2 | ||
|
|
6669633902 | ||
|
|
a65d282c0b | ||
|
|
524b43cc40 | ||
|
|
4e0b27c4d7 | ||
|
|
c6d7acfd99 | ||
|
|
bf941c71ec | ||
|
|
dcec510451 | ||
|
|
cb468b0f3c | ||
|
|
6713510ce9 | ||
|
|
bf5b32de16 | ||
|
|
2a1780b43d | ||
|
|
be05ec99b5 | ||
|
|
3a42bc034d | ||
|
|
27e7664ef0 | ||
|
|
ccbf1d3269 | ||
|
|
0ca14739f9 |
+13
-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).
|
||||
|
||||
> **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.2**.
|
||||
|
||||
---
|
||||
|
||||
@@ -14,6 +14,18 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
---
|
||||
|
||||
## [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
|
||||
|
||||
---
|
||||
|
||||
+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.
|
||||
|
||||
[]()
|
||||
[]()
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://www.docker.com/)
|
||||
[](https://www.python.org/)
|
||||
@@ -977,8 +977,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l
|
||||
|
||||
## 📝 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.
|
||||
|
||||
[]()
|
||||
[]()
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://www.docker.com/)
|
||||
[](https://www.python.org/)
|
||||
@@ -1152,8 +1152,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE)
|
||||
|
||||
## 📝 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*
|
||||
|
||||
@@ -41,6 +41,7 @@ TAGS_METADATA: list[dict[str, str]] = [
|
||||
{"name": "Webhooks", "description": "HTTP callbacks signed with HMAC-SHA256 for file events."},
|
||||
{"name": "Conflicts", "description": "Detect and resolve Syncthing sync-conflict files."},
|
||||
{"name": "Admin", "description": "Admin-only system monitoring: stats, audit log, backup stats and live stream."},
|
||||
{"name": "Coffres Secrets", "description": "Secret vaults: encrypted coffres, secrets, consumers, grants and distribution — metadata only, values are never exposed."},
|
||||
{"name": "Plugins", "description": "Install, enable and manage user plugins."},
|
||||
{"name": "Push", "description": "Web Push (VAPID) subscription management and test notifications."},
|
||||
{"name": "Duplicates", "description": "Duplicate-note detection and confirmed merge (#166)."},
|
||||
|
||||
Generated
+1
-1
@@ -2626,7 +2626,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "obsigate-desktop"
|
||||
version = "2.81.0"
|
||||
version = "2.81.2"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"env_logger",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "obsigate-desktop"
|
||||
version = "2.81.0"
|
||||
version = "2.81.2"
|
||||
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
|
||||
authors = ["Bruno Charest"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
|
||||
"productName": "ObsiGate",
|
||||
"version": "2.81.0",
|
||||
"version": "2.81.2",
|
||||
"identifier": "com.obsigate.desktop",
|
||||
"build": {
|
||||
"frontendDist": "../frontend",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# ObsiGate — Roadmap
|
||||
|
||||
> **Version :** 2.81.0 | **Dernière mise à jour :** 2026-10-28
|
||||
> **Version :** 2.81.2 | **Dernière mise à jour :** 2026-10-28
|
||||
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
|
||||
> vers les fonctionnalités livrées.
|
||||
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
|
||||
|
||||
+89
-17
@@ -37,16 +37,19 @@
|
||||
display: flex; align-items: center; gap: 9px;
|
||||
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-hd .soustitre { font-size: 12px; color: var(--p-doux); }
|
||||
.draco-panneau-titres { min-width: 0; flex: 1; }
|
||||
.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 {
|
||||
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) */
|
||||
.draco-disc-bar {
|
||||
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.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-panneau-ft { border-top: 1px solid var(--p-bord); padding: 11px 12px; background: var(--p-surface); }
|
||||
.draco-panneau-form { display: flex; gap: 8px; }
|
||||
.draco-panneau-form input {
|
||||
flex: 1; border: 1px solid var(--p-chip-bord); border-radius: 10px; padding: 9px 11px;
|
||||
font: inherit; background: var(--p-champ); color: var(--p-texte);
|
||||
.draco-panneau-ft { padding: 10px 12px 11px; background: var(--p-fond); }
|
||||
/* Puces de suggestions, comme la console : au-dessus du composer. */
|
||||
.draco-puces {
|
||||
display: flex; flex-wrap: wrap; gap: 7px; justify-content: center; padding: 0 2px 8px;
|
||||
}
|
||||
.draco-panneau-form button {
|
||||
border: 0; border-radius: 10px; background: var(--p-accent); color: #fff;
|
||||
padding: 9px 14px; font: inherit; font-weight: 600; cursor: pointer;
|
||||
.draco-puces button {
|
||||
border: 1px solid var(--p-chip-bord); background: var(--p-chip); color: var(--p-texte);
|
||||
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 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; }
|
||||
|
||||
+332
-23
@@ -40,7 +40,10 @@
|
||||
var convKind = null; // 'principale' | 'parallele'
|
||||
var convTitre = "";
|
||||
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 microEl = null, piecesEl = null, fichierEl = null;
|
||||
var titreEl = null, tiroirEl = null;
|
||||
var versionEl = null, versionConnue = "", versionMaj = 0;
|
||||
|
||||
@@ -68,11 +71,16 @@
|
||||
return correspondance[h] || null;
|
||||
}
|
||||
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 id = convId || convStockee();
|
||||
if (id) params.push("disc=" + encodeURIComponent(id));
|
||||
var app = appHote();
|
||||
if (app) params.push("app=" + encodeURIComponent(app));
|
||||
params.push("domain=" + encodeURIComponent(domaine()));
|
||||
return API + "/" + (params.length ? "?" + params.join("&") : "");
|
||||
}
|
||||
|
||||
@@ -147,7 +155,15 @@
|
||||
|
||||
function ouvrirConsole() {
|
||||
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;
|
||||
surOuverte = true;
|
||||
/* Naissance à la géométrie du panneau (ancrée à droite), puis
|
||||
@@ -175,7 +191,12 @@
|
||||
setTimeout(terminer, 380); // filet si transitionend ne vient pas
|
||||
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(); }
|
||||
|
||||
/* ── 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-panneau-hd">' +
|
||||
' <span style="font-size:19px;color:#7c5cd6">✦</span>' +
|
||||
' <div><div class="titre">Spark — agent commun</div>' +
|
||||
' <div class="soustitre">Service d\'agents dracodev</div></div>' +
|
||||
' <div class="draco-panneau-titres"><div class="titre">Spark — agent commun</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>' +
|
||||
'</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">' +
|
||||
' <span class="d-titre">Discussion principale</span>' +
|
||||
' <button type="button" class="d-btn" data-disc="liste">☰ Discussions</button>' +
|
||||
@@ -266,17 +284,40 @@
|
||||
' <button type="button" data-outil="missions">🚀 Missions</button>' +
|
||||
'</div>' +
|
||||
'<div class="draco-panneau-msgs"></div>' +
|
||||
'<div class="draco-panneau-ft"><form class="draco-panneau-form">' +
|
||||
' <input type="text" placeholder="Écrire à Spark…" autocomplete="off" aria-label="Message à Spark">' +
|
||||
' <button type="submit">Envoyer</button></form>' +
|
||||
' <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>' +
|
||||
'<div class="draco-panneau-ft">' +
|
||||
' <div class="draco-puces">' +
|
||||
' <button type="button" data-sugg="resume">Résumer ma journée</button>' +
|
||||
' <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>';
|
||||
document.body.appendChild(root);
|
||||
msgsEl = root.querySelector(".draco-panneau-msgs");
|
||||
etatEl = root.querySelector(".draco-panneau-etat");
|
||||
inputEl = root.querySelector("input");
|
||||
sendBtn = root.querySelector("button[type=submit]");
|
||||
etatEl = root.querySelector(".draco-panneau-hd .soustitre");
|
||||
inputEl = root.querySelector(".draco-composer textarea");
|
||||
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");
|
||||
tiroirEl = root.querySelector(".draco-discussions");
|
||||
versionEl = root.querySelector(".draco-panneau-version");
|
||||
@@ -288,6 +329,42 @@
|
||||
root.querySelector('[data-outil="approbations"]').addEventListener("click", chargerApprobations);
|
||||
root.querySelector('[data-outil="missions"]').addEventListener("click", ouvrirMissions);
|
||||
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"));
|
||||
appliquerTheme();
|
||||
surveillerTheme();
|
||||
@@ -326,8 +403,13 @@
|
||||
}
|
||||
|
||||
function majEtat(cls, texte) {
|
||||
etatEl.className = "draco-panneau-hd draco-panneau-etat " + cls;
|
||||
etatEl.querySelector(".txt").textContent = texte;
|
||||
/* Statut intégré au titre (sous-titre de l'en-tête) : plus de
|
||||
* 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() {
|
||||
titreEl.textContent = convTitre || "Discussion principale";
|
||||
@@ -400,9 +482,21 @@
|
||||
.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() {
|
||||
chargerListeDiscussions()
|
||||
.then(function (d) {
|
||||
domaineCharge = domaine();
|
||||
var precedente = convStockee();
|
||||
var candidates = (d.paralleles || []).concat(d.principale ? [d.principale] : []);
|
||||
var trouvee = candidates.filter(function (c) { return c.id === precedente; })[0];
|
||||
@@ -413,6 +507,7 @@
|
||||
chargerHistorique();
|
||||
})
|
||||
.catch(function () {
|
||||
domaineCharge = null; // échec : la prochaine ouverture réessaiera
|
||||
ajouterMsg("sys", "Discussions : service injoignable pour le moment.");
|
||||
});
|
||||
}
|
||||
@@ -780,19 +875,232 @@
|
||||
.catch(function (e) { ajouterMsg("sys", "Lancement refusé : " + e.message); });
|
||||
}
|
||||
|
||||
function envoyer(ev) {
|
||||
ev.preventDefault();
|
||||
var texte = inputEl.value.trim();
|
||||
if (!texte) return;
|
||||
/* ── Composer façon console ────────────────────────────────
|
||||
* Mêmes règles que la console : + (images/documents, 4 max),
|
||||
* textarea auto-extensible, micro affiché seulement sans texte
|
||||
* 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 = "";
|
||||
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;
|
||||
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", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ message: texte, domain: domaine(), conversation_id: convId })
|
||||
body: JSON.stringify(corps)
|
||||
})
|
||||
.then(function (r) {
|
||||
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.";
|
||||
majEtat("err", "Service d'agents injoignable");
|
||||
})
|
||||
.finally(function () { sendBtn.disabled = false; inputEl.focus(); });
|
||||
.finally(function () { sendBtn.disabled = false; majComposer(); inputEl.focus(); });
|
||||
}
|
||||
|
||||
function ouvrir() {
|
||||
if (!root) { construire(); demarrerDiscussions(); }
|
||||
else resynchroniserDomaine();
|
||||
root.classList.add("open");
|
||||
appliquerTheme();
|
||||
verifierIdentite();
|
||||
|
||||
@@ -59,11 +59,11 @@ function _sharedIcon(size = 16) {
|
||||
// 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() {
|
||||
const s = document.createElement("span");
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -653,7 +653,7 @@ function applyTheme(themeKey, mode) {
|
||||
root.style.setProperty(key, vars[key]);
|
||||
});
|
||||
// #180 — the spreadsheet grid follows the active theme: derive its tokens
|
||||
// from the generic surfaces so every theme (15 built-in × 4 modes, custom
|
||||
// from the generic surfaces so every theme (16 built-in × 4 modes, custom
|
||||
// imports) gets a matching grid instead of the hardcoded dark/light pair.
|
||||
// color-mix keeps the zebra stripe subtle on both dark and light grounds.
|
||||
// #185 — `--grid-border` no longer reuses `--border` (which sits 1-2% off the
|
||||
|
||||
+2
-1
@@ -14372,7 +14372,8 @@ body.has-draco-bar .app-container { height: calc(100vh - 60px); }
|
||||
.coffres-alerts { margin: 12px 0; }
|
||||
.coffres-alert { color: var(--warning, #f59e0b); padding: 2px 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-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; }
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsigate",
|
||||
"version": "2.81.0",
|
||||
"version": "2.81.2",
|
||||
"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",
|
||||
"directories": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Validates:
|
||||
- Theme JSON structure validity
|
||||
- All 15 built-in themes have required keys and valid CSS colors
|
||||
- All 16 built-in themes have required keys and valid CSS colors
|
||||
- High-contrast and sepia mode generators produce valid CSS vars
|
||||
- Import/export round-trip
|
||||
- AVAILABLE_MODES includes all 4 modes
|
||||
@@ -80,9 +80,9 @@ class TestThemeStructure:
|
||||
assert '_genSepia' in self.content
|
||||
|
||||
def test_theme_count(self):
|
||||
"""Should have 15 built-in themes."""
|
||||
"""Should have 16 built-in themes."""
|
||||
names = _extract_theme_names(self.content)
|
||||
assert len(names) == 15, f"Expected 15 themes, found {len(names)}: {names}"
|
||||
assert len(names) == 16, f"Expected 16 themes, found {len(names)}: {names}"
|
||||
|
||||
def test_required_export_functions(self):
|
||||
"""All required functions should be exported."""
|
||||
|
||||
Reference in New Issue
Block a user