Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
943005328c |
@@ -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.24.0**.
|
> [Unreleased](#unreleased). La dernière version livrée est **2.25.0**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -14,6 +14,34 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [2.25.0] — 2026-09-24
|
||||||
|
|
||||||
|
### Ajouté
|
||||||
|
|
||||||
|
- **#115 — barre d'outils de lecture toujours visible.** La barre d'actions d'un
|
||||||
|
document (pop-out, bookmark, Editer, Source, Copier, Partager…) est désormais
|
||||||
|
**épinglée en haut** de la zone de lecture : elle reste accessible en permanence,
|
||||||
|
même au bas d'un document long, au lieu de disparaître au défilement. Elle est
|
||||||
|
rendue comme un enfant direct du conteneur de défilement (`.file-toolbar`), masquée
|
||||||
|
en mode lecture, et alignée dans la fenêtre pop-out.
|
||||||
|
- **#117 — avatars prédéfinis dans le profil.** La section **Profil** propose une
|
||||||
|
galerie de **12 avatars** fournis avec l'application (`frontend/icons/avatar/`) :
|
||||||
|
un clic charge l'image, la recadre au format carré 256 px (même pipeline que
|
||||||
|
l'import) et l'enregistre sur le compte. L'avatar actif est surligné ; l'import
|
||||||
|
d'une photo personnalisée et la suppression restent disponibles.
|
||||||
|
|
||||||
|
### Corrigé
|
||||||
|
|
||||||
|
- **BUG-078 — coloration syntaxique des fichiers de code perdue.** Les feuilles de
|
||||||
|
style highlight.js étaient basculées à partir de la **clé de thème**
|
||||||
|
(`defaut-obsigate`, …) au lieu du **mode** (`dark`/`light`) : les deux feuilles se
|
||||||
|
retrouvaient désactivées et les blocs de code (`.py`, `.sh`, `.ps1`, `.yml`, JSON,
|
||||||
|
blocs Markdown…) s'affichaient en texte brut. Le basculement est désormais piloté
|
||||||
|
par le mode, de façon déterministe, dans le moteur de thème (`themes.js`) comme au
|
||||||
|
premier rendu (`ui.js`) ; sépia et contraste élevé réutilisent la palette claire.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [2.24.0] — 2026-09-24
|
## [2.24.0] — 2026-09-24
|
||||||
|
|
||||||
### Ajouté
|
### Ajouté
|
||||||
|
|||||||
@@ -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/)
|
||||||
@@ -975,8 +975,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.24.0).
|
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.25.0).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Projet : ObsiGate | Version : 2.24.0 | Dernière mise à jour : Septembre 2026*
|
*Projet : ObsiGate | Version : 2.25.0 | 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/)
|
||||||
@@ -1150,8 +1150,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.24.0).
|
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.25.0).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Project: ObsiGate | Version: 2.24.0 | Last updated: September 2026*
|
*Project: ObsiGate | Version: 2.25.0 | Last updated: September 2026*
|
||||||
|
|||||||
@@ -2626,7 +2626,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "obsigate-desktop"
|
name = "obsigate-desktop"
|
||||||
version = "2.24.0"
|
version = "2.25.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "obsigate-desktop"
|
name = "obsigate-desktop"
|
||||||
version = "2.24.0"
|
version = "2.25.0"
|
||||||
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.24.0",
|
"version": "2.25.0",
|
||||||
"identifier": "com.obsigate.desktop",
|
"identifier": "com.obsigate.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../frontend",
|
"frontendDist": "../frontend",
|
||||||
|
|||||||
@@ -187,6 +187,7 @@ Avant de corriger quoi que ce soit, un agent IA doit :
|
|||||||
| *BUG-075* | [🟡 IMPORTANT] Assistant IA : chaque action mutatrice demande son propre « Appliquer » — aucun résumé des actions en attente ni approbation globale | 🟢 corrigé | P1 | ⚙️ backend + 📱 frontend | IA | `backend/agent/loop.py`, `backend/bookslm_routes.py`, `frontend/js/bookslm.js` | Mode agent : demander une structure de répertoires multi-fichiers → valider une action après l'autre | `backend/agent/loop.py` (`pending.actions`, lot exécuté au resume) ; `backend/bookslm_routes.py` (`confirm_all` → `ctx.confirmed`) ; `frontend/js/bookslm.js` (carte multi-actions + « Tout approuver (N) »). Tests : `tests/test_agent_loop.py`, `tests/test_bookslm.py`, `tests/frontend/ai.test.mjs` | La pause de confirmation ne capture que le **premier** appel mutateur du lot (les suivants sont `deferred`) ; carte unique sans liste ; nouveau `confirm_all` à ajouter pour autoriser la suite de l'exécution en une approbation |
|
| *BUG-075* | [🟡 IMPORTANT] Assistant IA : chaque action mutatrice demande son propre « Appliquer » — aucun résumé des actions en attente ni approbation globale | 🟢 corrigé | P1 | ⚙️ backend + 📱 frontend | IA | `backend/agent/loop.py`, `backend/bookslm_routes.py`, `frontend/js/bookslm.js` | Mode agent : demander une structure de répertoires multi-fichiers → valider une action après l'autre | `backend/agent/loop.py` (`pending.actions`, lot exécuté au resume) ; `backend/bookslm_routes.py` (`confirm_all` → `ctx.confirmed`) ; `frontend/js/bookslm.js` (carte multi-actions + « Tout approuver (N) »). Tests : `tests/test_agent_loop.py`, `tests/test_bookslm.py`, `tests/frontend/ai.test.mjs` | La pause de confirmation ne capture que le **premier** appel mutateur du lot (les suivants sont `deferred`) ; carte unique sans liste ; nouveau `confirm_all` à ajouter pour autoriser la suite de l'exécution en une approbation |
|
||||||
| *BUG-076* | [🟡 IMPORTANT] Assistant IA : après une action de l'agent, l'arborescence et le document ouvert ne sont pas rafraîchis dynamiquement | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/bookslm.js` | Mode agent : créer/supprimer un fichier ou dossier, modifier le document ouvert → l'UI ne bouge pas | `frontend/js/bookslm.js` : `MUTATING_TOOLS`/`FILE_WRITE_TOOLS`, refresh d'arborescence débouncé sur event `tool`, `_notifyFileWritten` étendu (xlsx/docx/csv/pdf). Tests : `tests/frontend/ai.test.mjs`, `tests/frontend/editor-inline.test.mjs` | Aucun refresh explicite sur les événements `tool` mutateurs (repose uniquement sur le watcher SSE) ; `_notifyFileWritten` ignore les créations de documents (xlsx/docx/csv/pdf) |
|
| *BUG-076* | [🟡 IMPORTANT] Assistant IA : après une action de l'agent, l'arborescence et le document ouvert ne sont pas rafraîchis dynamiquement | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/bookslm.js` | Mode agent : créer/supprimer un fichier ou dossier, modifier le document ouvert → l'UI ne bouge pas | `frontend/js/bookslm.js` : `MUTATING_TOOLS`/`FILE_WRITE_TOOLS`, refresh d'arborescence débouncé sur event `tool`, `_notifyFileWritten` étendu (xlsx/docx/csv/pdf). Tests : `tests/frontend/ai.test.mjs`, `tests/frontend/editor-inline.test.mjs` | Aucun refresh explicite sur les événements `tool` mutateurs (repose uniquement sur le watcher SSE) ; `_notifyFileWritten` ignore les créations de documents (xlsx/docx/csv/pdf) |
|
||||||
| *BUG-077* | [🟡 IMPORTANT] Assistant IA : aucun bouton « Stop » pour arrêter l'exécution de l'agent à tout moment | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/bookslm.js` | Mode agent : lancer une longue tâche → le bouton Envoyer est désactivé, impossible d'arrêter (seule la fermeture du panneau abort) | `frontend/js/bookslm.js` + `frontend/style.css` : bouton d'envoi → Stop (`_syncSendButton`/`_stopGeneration`/`_markStopped`), i18n `ai.stop`/`ai.stopped`. Tests : `tests/frontend/ai.test.mjs` (+2) | `_abortCtrl` n'est déclenché que par `close()` ; aucun signal d'arrêt côté client pendant le stream |
|
| *BUG-077* | [🟡 IMPORTANT] Assistant IA : aucun bouton « Stop » pour arrêter l'exécution de l'agent à tout moment | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/bookslm.js` | Mode agent : lancer une longue tâche → le bouton Envoyer est désactivé, impossible d'arrêter (seule la fermeture du panneau abort) | `frontend/js/bookslm.js` + `frontend/style.css` : bouton d'envoi → Stop (`_syncSendButton`/`_stopGeneration`/`_markStopped`), i18n `ai.stop`/`ai.stopped`. Tests : `tests/frontend/ai.test.mjs` (+2) | `_abortCtrl` n'est déclenché que par `close()` ; aucun signal d'arrêt côté client pendant le stream |
|
||||||
|
| *BUG-078* | [🟡 IMPORTANT] Fichiers de code : la coloration syntaxique (highlight.js) disparaît — les feuilles de thème sont basculées à partir de la **clé** de thème au lieu du **mode** | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/themes.js`, `frontend/js/ui.js`, `tests/frontend/unit.test.mjs` | Ouvrir un fichier `.py`/`.sh`/`.ps1`/`.yml` : le code s'affiche en texte brut, sans couleurs | `frontend/js/themes.js` : `applyTheme` bascule `hljs-theme-dark`/`hljs-theme-light` selon le **mode** (`isDark`). `frontend/js/ui.js` : `initTheme`/`applyTheme` résolvent le mode persisté (`obsigate-theme-mode`) au lieu de traiter la clé (`defaut-obsigate`) comme un mode. Test : `unit.test.mjs` (+1). | Les deux feuilles étaient désactivées car `defaut-obsigate !== "dark"` et `!== "light"` ; résultat **non déterministe** selon l'ordre `UI.initTheme()` (clé) / `Sync.init()` → `themes.initThemes()` (mode). Vérifié Playwright : 5/5 chargements colorés (`.py`), sépia/contraste élevé sur la palette claire |
|
||||||
|
|
||||||
### TODOs techniques (améliorations / nouvelles tâches)
|
### TODOs techniques (améliorations / nouvelles tâches)
|
||||||
|
|
||||||
@@ -269,6 +270,8 @@ Avant de corriger quoi que ce soit, un agent IA doit :
|
|||||||
| 2026-09-24 | #114 | Feature | `frontend/index.html`, `frontend/js/config.js`, `frontend/style.css`, `frontend/locales/{fr,en}.json`, `tests/frontend/config-mobile.test.mjs`, `tests/e2e/config-mobile.spec.js`, `docs/features/settings-mobile-114.md` (nouvelle), `docs/ROADMAP.md`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **#114 — Configuration, refonte mobile-responsive (≤768px)**. (1) **Drawer sommaire** : `#config-nav` en panneau `position: fixed` (`min(320px, 88vw)`, z-index 40) sous backdrop `#config-modal.config-toc-open::before` (z-index 35) ; bouton `#config-toc-close` ; backdrop/Échap ferment le drawer d'abord puis la modale ; `_setConfigNav` bascule la classe + conserve le `display` inline de reset. (2) **Modale plein écran** : `100dvh` + `padding: 0`, `border-radius: 0`. (3) **Tactile** : boutons/liens ≥44px, inputs/selects `16px` + `min-height: 44px` (anti-zoom iOS, scopé `#config-modal`), rangée `.config-actions-row` sticky column + safe-area, formulaires 1 colonne, MFA 1 colonne + code full-width, wrap webhook/token/share/diag/avatar/webauthn. (4) **Dettes HTML/i18n** : `.config-actions-row` replacée dans `#cfg-backend-settings` (`</section>` orphelin supprimé), id dupliqué `cfg-partages-publics` retiré du `<h2>`, `#plugins-settings-container` supprimé, doublons `.config-btn-add` + règle morte `.mfa-recovery-input` purgés, `#mt-explorer` → `data-i18n="settings.explorer"` ; i18n : clés mortes `settings.{backend,backend_hint,restart_badge,save,plugins}` supprimées, `settings.explorer` + `config.toc_close` ajoutées, `settings.tabs` FR = « Onglets ». Vérifié : `config-mobile.test.mjs` 27/27 (au CI), unit 11/11, validate-imports 40 modules, pytest 1302 passed / 6 skipped, ruff/mypy 0, E2E `chromium-mobile` 5/5. | ✅ livré (en attente vérif utilisateur) |
|
| 2026-09-24 | #114 | Feature | `frontend/index.html`, `frontend/js/config.js`, `frontend/style.css`, `frontend/locales/{fr,en}.json`, `tests/frontend/config-mobile.test.mjs`, `tests/e2e/config-mobile.spec.js`, `docs/features/settings-mobile-114.md` (nouvelle), `docs/ROADMAP.md`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **#114 — Configuration, refonte mobile-responsive (≤768px)**. (1) **Drawer sommaire** : `#config-nav` en panneau `position: fixed` (`min(320px, 88vw)`, z-index 40) sous backdrop `#config-modal.config-toc-open::before` (z-index 35) ; bouton `#config-toc-close` ; backdrop/Échap ferment le drawer d'abord puis la modale ; `_setConfigNav` bascule la classe + conserve le `display` inline de reset. (2) **Modale plein écran** : `100dvh` + `padding: 0`, `border-radius: 0`. (3) **Tactile** : boutons/liens ≥44px, inputs/selects `16px` + `min-height: 44px` (anti-zoom iOS, scopé `#config-modal`), rangée `.config-actions-row` sticky column + safe-area, formulaires 1 colonne, MFA 1 colonne + code full-width, wrap webhook/token/share/diag/avatar/webauthn. (4) **Dettes HTML/i18n** : `.config-actions-row` replacée dans `#cfg-backend-settings` (`</section>` orphelin supprimé), id dupliqué `cfg-partages-publics` retiré du `<h2>`, `#plugins-settings-container` supprimé, doublons `.config-btn-add` + règle morte `.mfa-recovery-input` purgés, `#mt-explorer` → `data-i18n="settings.explorer"` ; i18n : clés mortes `settings.{backend,backend_hint,restart_badge,save,plugins}` supprimées, `settings.explorer` + `config.toc_close` ajoutées, `settings.tabs` FR = « Onglets ». Vérifié : `config-mobile.test.mjs` 27/27 (au CI), unit 11/11, validate-imports 40 modules, pytest 1302 passed / 6 skipped, ruff/mypy 0, E2E `chromium-mobile` 5/5. | ✅ livré (en attente vérif utilisateur) |
|
||||||
| 2026-09-24 | BUG-074 → BUG-077 | Correction | `backend/agent/loop.py`, `backend/bookslm_routes.py`, `frontend/js/bookslm.js`, `frontend/style.css`, `frontend/index.html`, `frontend/locales/{fr,en}.json`, `frontend/sw.js`, `tests/test_agent_loop.py`, `tests/test_bookslm.py`, `tests/frontend/ai.test.mjs`, `tests/frontend/editor-inline.test.mjs`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **Lot assistant IA (mode agent)** : (BUG-075) confirmation par lot — `pending.actions` regroupe toutes les mutations d'un tour LLM, un unique bouton « Tout approuver (N) » envoie `confirm_all` (`ToolContext.confirmed`) et n'interrompt plus à chaque action ; les lectures du lot s'exécutent aussitôt. (BUG-074) résumé du bloc d'étapes avec titre de la 1re action + compteur limité aux actions, reprise diffusée dans le même message (fini le « 1 étape » fragmenté). (BUG-076) refresh de l'arborescence débouncé sur les events `tool` mutateurs + `_notifyFileWritten` étendu aux documents (xlsx/docx/csv/pdf). (BUG-077) le bouton d'envoi devient « Stop » pendant le stream (abort SSE, tâche serveur annulée à la déconnexion, marqueur « Exécution arrêtée. »). Guide/i18n FR/EN + `ai.stop`/`ai.stopped`/`ai.confirm_actions`/`ai.action_apply_all` ; `SW_VERSION` v25. Vérifié : pytest 1304 passed / 6 skipped, ruff/mypy 0, validate-imports 40 modules, unit 11/11, ai 100/100, editor-inline 44/44, mobile-editor 35/35, ai-sidebar 6/6, forge 32/32, pane-manager 9/9, sw 8/8. | 🟢 corrigé (en attente vérif utilisateur) |
|
| 2026-09-24 | BUG-074 → BUG-077 | Correction | `backend/agent/loop.py`, `backend/bookslm_routes.py`, `frontend/js/bookslm.js`, `frontend/style.css`, `frontend/index.html`, `frontend/locales/{fr,en}.json`, `frontend/sw.js`, `tests/test_agent_loop.py`, `tests/test_bookslm.py`, `tests/frontend/ai.test.mjs`, `tests/frontend/editor-inline.test.mjs`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **Lot assistant IA (mode agent)** : (BUG-075) confirmation par lot — `pending.actions` regroupe toutes les mutations d'un tour LLM, un unique bouton « Tout approuver (N) » envoie `confirm_all` (`ToolContext.confirmed`) et n'interrompt plus à chaque action ; les lectures du lot s'exécutent aussitôt. (BUG-074) résumé du bloc d'étapes avec titre de la 1re action + compteur limité aux actions, reprise diffusée dans le même message (fini le « 1 étape » fragmenté). (BUG-076) refresh de l'arborescence débouncé sur les events `tool` mutateurs + `_notifyFileWritten` étendu aux documents (xlsx/docx/csv/pdf). (BUG-077) le bouton d'envoi devient « Stop » pendant le stream (abort SSE, tâche serveur annulée à la déconnexion, marqueur « Exécution arrêtée. »). Guide/i18n FR/EN + `ai.stop`/`ai.stopped`/`ai.confirm_actions`/`ai.action_apply_all` ; `SW_VERSION` v25. Vérifié : pytest 1304 passed / 6 skipped, ruff/mypy 0, validate-imports 40 modules, unit 11/11, ai 100/100, editor-inline 44/44, mobile-editor 35/35, ai-sidebar 6/6, forge 32/32, pane-manager 9/9, sw 8/8. | 🟢 corrigé (en attente vérif utilisateur) |
|
||||||
|
|
||||||
|
| 2026-09-24 | #115, #117, BUG-078 | Feature + correction | `frontend/js/themes.js`, `frontend/js/ui.js`, `frontend/js/viewer.js`, `frontend/js/config.js`, `frontend/index.html`, `frontend/style.css`, `frontend/popout.html`, `frontend/locales/{fr,en}.json`, `frontend/icons/avatar/*` (nouveau), `tests/frontend/unit.test.mjs`, `tests/frontend/toolbar-order.test.mjs`, `tests/frontend/settings-order-avatar.test.mjs`, `docs/features/viewer-toolbar-highlight-avatars.md` (nouvelle), `docs/ROADMAP.md`, `CHANGELOG.md` | **#115** barre d'outils de lecture épinglée : `viewer.js`/`popout.html` sortent `.file-actions` de `.file-header` dans un `.file-toolbar` enfant direct de `.content-area` (`position: sticky; top: 0`), masqué en mode lecture. **BUG-078** coloration syntaxique : le basculement des feuilles highlight.js suit le **mode** (`themes.applyTheme` + `ui.initTheme/applyTheme` lisent `obsigate-theme-mode`) au lieu de la clé de thème qui désactivait les deux feuilles. **#117** avatars prédéfinis : galerie de 12 images (`frontend/icons/avatar/`) dans `#cfg-profile`, clic → recadrage 256 px (pipeline import) + `PATCH /api/auth/me`, avatars actifs surlignés (`obsigate-avatar-preset`), import personnalisé et suppression conservés. Vérifié : Playwright (coloration 5/5 déterministe, toolbar épinglée à `barTop` constant au défilement), `unit.test.mjs` 12/12, `toolbar-order` 13/13, `settings-order-avatar` 12/12, JSDOM editor-inline/pane-manager/mobile-editor/image-viewer/pdf-viewer/config-mobile/media-viewer/excalidraw verts, pytest 1304 passed / 6 skipped, ruff/mypy 0, validate-imports 40 modules. | 🟢 corrigé (en attente vérif utilisateur) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📜 Historique des bugs résolus
|
## 📜 Historique des bugs résolus
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# ObsiGate — Roadmap
|
# ObsiGate — Roadmap
|
||||||
|
|
||||||
> **Version :** 2.24.0 | **Dernière mise à jour :** 2026-09-24
|
> **Version :** 2.25.0 | **Dernière mise à jour :** 2026-09-24
|
||||||
> **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)**
|
||||||
@@ -189,6 +189,9 @@
|
|||||||
| 112 | En-tête allégé & compte en sidebar — version dans le menu Options, retrait utilisateur/déconnexion du header, section compte en bas de la sidebar, pellicule d'images défilable (molette + flèches) | 2.21.0 | [features/header-user-sidebar-112.md](./features/header-user-sidebar-112.md) |
|
| 112 | En-tête allégé & compte en sidebar — version dans le menu Options, retrait utilisateur/déconnexion du header, section compte en bas de la sidebar, pellicule d'images défilable (molette + flèches) | 2.21.0 | [features/header-user-sidebar-112.md](./features/header-user-sidebar-112.md) |
|
||||||
| 113 | Configuration — ordre naturel des sections (Profil 1er, À propos dernier, TOC = page) & avatar utilisateur (import PNG/JPG/WEBP, persistance serveur, cercle sidebar) | 2.22.0 | [features/settings-order-avatar-113.md](./features/settings-order-avatar-113.md) |
|
| 113 | Configuration — ordre naturel des sections (Profil 1er, À propos dernier, TOC = page) & avatar utilisateur (import PNG/JPG/WEBP, persistance serveur, cercle sidebar) | 2.22.0 | [features/settings-order-avatar-113.md](./features/settings-order-avatar-113.md) |
|
||||||
| 114 | Configuration — refonte mobile-responsive (modale plein écran 100dvh, sommaire en drawer coulissant, cibles tactiles ≥ 44 px, inputs 16 px anti-zoom, sauvegarde sticky, MFA 1 colonne, purge i18n/HTML) | 2.23.0 | [features/settings-mobile-114.md](./features/settings-mobile-114.md) |
|
| 114 | Configuration — refonte mobile-responsive (modale plein écran 100dvh, sommaire en drawer coulissant, cibles tactiles ≥ 44 px, inputs 16 px anti-zoom, sauvegarde sticky, MFA 1 colonne, purge i18n/HTML) | 2.23.0 | [features/settings-mobile-114.md](./features/settings-mobile-114.md) |
|
||||||
|
| BUG-078 | Fichiers de code — coloration syntaxique restaurée (feuilles highlight.js basculées sur le mode de thème et non la clé) | 2.25.0 | [features/viewer-toolbar-highlight-avatars.md](./features/viewer-toolbar-highlight-avatars.md) |
|
||||||
|
| 115 | Viewer — barre d'outils de lecture épinglée au défilement | 2.25.0 | [features/viewer-toolbar-highlight-avatars.md](./features/viewer-toolbar-highlight-avatars.md) |
|
||||||
|
| 117 | Configuration — avatars prédéfinis dans le profil utilisateur (12 images) | 2.25.0 | [features/viewer-toolbar-highlight-avatars.md](./features/viewer-toolbar-highlight-avatars.md) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
# #115 / BUG-078 / #117 — Barre d'outils épinglée, coloration syntaxique des fichiers de code & avatars prédéfinis
|
||||||
|
|
||||||
|
> **Statut :** 🟢 livré (en attente vérification utilisateur)
|
||||||
|
> **Impact :** 🟡 (#115, BUG-078) · 🟢 (#117)
|
||||||
|
> **Zone :** frontend (`frontend/js/viewer.js`, `frontend/js/themes.js`,
|
||||||
|
> `frontend/js/ui.js`, `frontend/js/config.js`, `frontend/index.html`,
|
||||||
|
> `frontend/style.css`, `frontend/popout.html`, `frontend/locales/fr.json`,
|
||||||
|
> `frontend/locales/en.json`, `frontend/icons/avatar/*`)
|
||||||
|
|
||||||
|
Trois demandes traitées dans la même livraison, toutes côté frontend.
|
||||||
|
|
||||||
|
## #115 — Barre d'outils de lecture toujours visible
|
||||||
|
|
||||||
|
### Problème
|
||||||
|
|
||||||
|
La barre d'actions d'un document (pop-out, bookmark, Editer, Source, Copier, PDF, Export,
|
||||||
|
Partager…) était rendue **dans** `.file-header`, un conteneur court placé en haut de la
|
||||||
|
zone de lecture. Or un élément `position: sticky` reste borné par son parent : dès que
|
||||||
|
`.file-header` sortait de l'écran au défilement d'un document long, la barre disparaissait.
|
||||||
|
|
||||||
|
### Correctif
|
||||||
|
|
||||||
|
- `frontend/js/viewer.js` et `frontend/popout.html` sortent la barre d'actions de
|
||||||
|
`.file-header` et la placent dans un nouveau conteneur `.file-toolbar`, **enfant direct
|
||||||
|
de `.content-area`** (le conteneur de défilement). La barre peut donc se coller au haut
|
||||||
|
de la zone de lecture pour toute la hauteur du document.
|
||||||
|
- `frontend/style.css` :
|
||||||
|
|
||||||
|
```css
|
||||||
|
.file-toolbar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 30;
|
||||||
|
margin: 0 0 16px;
|
||||||
|
padding: 8px 0;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Le fond est opaque pour qu'aucun texte ne transparaisse dessous.
|
||||||
|
- `body.reading-mode .file-toolbar { display: none }` : la barre reste masquée en mode
|
||||||
|
lecture, comme les autres actions.
|
||||||
|
|
||||||
|
## BUG-078 — Coloration syntaxique des fichiers de code
|
||||||
|
|
||||||
|
### Problème
|
||||||
|
|
||||||
|
Les fichiers `.py`, `.sh`, `.ps1`, `.yml`, `.json`… (et les blocs de code Markdown)
|
||||||
|
s'affichaient en **texte brut**, sans couleurs. Le code était pourtant correctement
|
||||||
|
généré côté backend (`<pre><code class="language-python">…`) et `safeHighlight()` appelait
|
||||||
|
bien `hljs.highlightElement()`.
|
||||||
|
|
||||||
|
La cause était ailleurs : les deux feuilles de style de highlight.js (`#hljs-theme-dark`
|
||||||
|
et `#hljs-theme-light`, chargées depuis le CDN dans `index.html`) étaient basculées à
|
||||||
|
partir de la **clé de thème** persistée (`obsigate-theme` = `defaut-obsigate`, …) :
|
||||||
|
|
||||||
|
```js
|
||||||
|
darkSheet.disabled = theme !== "dark"; // "defaut-obsigate" !== "dark" → true
|
||||||
|
lightSheet.disabled = theme !== "light"; // "defaut-obsigate" !== "light" → true
|
||||||
|
```
|
||||||
|
|
||||||
|
Les deux feuilles finissaient donc désactivées, privant tous les tokens de leurs couleurs.
|
||||||
|
Le résultat dépendait de l'ordre de deux initialisations concurrentes — `UI.initTheme()`
|
||||||
|
(clé de thème) au démarrage et `themes.initThemes()` (mode) via `Sync.init()` — d'où un
|
||||||
|
comportement **non déterministe** (parfois coloré, le plus souvent non).
|
||||||
|
|
||||||
|
### Correctif
|
||||||
|
|
||||||
|
- `frontend/js/themes.js` — `applyTheme(themeKey, mode)` bascule désormais les feuilles
|
||||||
|
highlight.js selon le **mode** :
|
||||||
|
|
||||||
|
```js
|
||||||
|
var isDark = mode === 'dark';
|
||||||
|
darkSheet.disabled = !isDark;
|
||||||
|
lightSheet.disabled = isDark;
|
||||||
|
```
|
||||||
|
|
||||||
|
(`sepia` et `high-contrast` réutilisent la palette claire.)
|
||||||
|
- `frontend/js/ui.js` — `initTheme()` lit le **mode** persisté (`obsigate-theme-mode`) et
|
||||||
|
`applyTheme()` résout un mode avant de fixer `data-theme` et de basculer les feuilles,
|
||||||
|
au lieu de comparer la clé de thème à `"dark"`/`"light"`.
|
||||||
|
|
||||||
|
Le basculement est ainsi **déterministe** dès le premier rendu et à chaque changement de
|
||||||
|
mode.
|
||||||
|
|
||||||
|
## #117 — Avatars prédéfinis dans le profil
|
||||||
|
|
||||||
|
### Ce qui a été livré
|
||||||
|
|
||||||
|
- **Galerie de 12 avatars** dans la section `Profil` (`#cfg-profile`), servis depuis
|
||||||
|
`frontend/icons/avatar/` (`/static/icons/avatar/<fichier>.jpg`) : Chat, chien, elephan,
|
||||||
|
hibou, koala, lapin, lion, ours, penda, pingouin, raton, tigre.
|
||||||
|
- Un clic charge l'image, la fait passer par le **même pipeline que l'import** (recadrage
|
||||||
|
carré central, redimensionnement 256 px, export JPEG 0,85) et l'enregistre via
|
||||||
|
`PATCH /api/auth/me` — aucune modification backend n'a été nécessaire, la validation
|
||||||
|
data-URL PNG/JPEG/WebP existante (BUG-113) s'applique telle quelle.
|
||||||
|
- L'avatar actif est **surligné** ; le choix est mémorisé dans `localStorage`
|
||||||
|
(`obsigate-avatar-preset`) et purgé dès qu'on importe une photo personnalisée ou qu'on
|
||||||
|
supprime l'avatar.
|
||||||
|
- L'import d'une photo et la suppression restent disponibles.
|
||||||
|
- i18n : nouvelle clé `config.avatar_presets_label` (FR/EN).
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
- `tests/frontend/unit.test.mjs` — `syntax highlight theme` (BUG-078) : `themes.applyTheme`
|
||||||
|
bascule les feuilles selon le mode et `ui.js` ne compare plus la clé au mode.
|
||||||
|
- `tests/frontend/toolbar-order.test.mjs` — #115 : `.file-toolbar` dans `viewer.js` et
|
||||||
|
`popout.html`, règle CSS `position: sticky; top: 0`, masquage en mode lecture.
|
||||||
|
- `tests/frontend/settings-order-avatar.test.mjs` — #117 : 12 avatars présents dans
|
||||||
|
`#cfg-profile`, fichiers d'images existants, pipeline `config.js`, règles CSS, clé i18n
|
||||||
|
FR/EN.
|
||||||
|
- Vérification Playwright (instance locale, auth désactivée) : coloration déterministe sur
|
||||||
|
5 chargements successifs d'un `.py` ; `.file-toolbar` dont le `top` ne bouge plus après
|
||||||
|
défilement (épinglage effectif).
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- L'avatar reste stocké en data-URL 256 px dans `data/users.json` (comportement #113
|
||||||
|
conservé).
|
||||||
|
- Le mode `high-contrast`/`sepia` utilise le thème highlight.js **clair** (pas de palette
|
||||||
|
dédiée).
|
||||||
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 159 KiB |
|
After Width: | Height: | Size: 143 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 143 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 159 KiB |
|
After Width: | Height: | Size: 194 KiB |
@@ -1660,6 +1660,27 @@
|
|||||||
<span class="profile-hint" data-i18n="config.avatar_hint">PNG, JPG ou WEBP — image carrée recadrée et réduite à 256 px.</span>
|
<span class="profile-hint" data-i18n="config.avatar_hint">PNG, JPG ou WEBP — image carrée recadrée et réduite à 256 px.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<span class="profile-avatar-label" data-i18n="config.avatar_presets_label">Avatars prédéfinis</span>
|
||||||
|
<div
|
||||||
|
class="profile-avatar-presets"
|
||||||
|
id="profile-avatar-presets"
|
||||||
|
role="radiogroup"
|
||||||
|
data-i18n-attr="aria-label:config.avatar_presets_label"
|
||||||
|
aria-label="Avatars prédéfinis"
|
||||||
|
>
|
||||||
|
<button type="button" class="profile-avatar-preset" data-avatar="Chat.jpg" role="radio" aria-checked="false"><img src="/static/icons/avatar/Chat.jpg" alt="" loading="lazy" /></button>
|
||||||
|
<button type="button" class="profile-avatar-preset" data-avatar="chien.jpg" role="radio" aria-checked="false"><img src="/static/icons/avatar/chien.jpg" alt="" loading="lazy" /></button>
|
||||||
|
<button type="button" class="profile-avatar-preset" data-avatar="elephan.jpg" role="radio" aria-checked="false"><img src="/static/icons/avatar/elephan.jpg" alt="" loading="lazy" /></button>
|
||||||
|
<button type="button" class="profile-avatar-preset" data-avatar="hibou.jpg" role="radio" aria-checked="false"><img src="/static/icons/avatar/hibou.jpg" alt="" loading="lazy" /></button>
|
||||||
|
<button type="button" class="profile-avatar-preset" data-avatar="koala.jpg" role="radio" aria-checked="false"><img src="/static/icons/avatar/koala.jpg" alt="" loading="lazy" /></button>
|
||||||
|
<button type="button" class="profile-avatar-preset" data-avatar="lapin.jpg" role="radio" aria-checked="false"><img src="/static/icons/avatar/lapin.jpg" alt="" loading="lazy" /></button>
|
||||||
|
<button type="button" class="profile-avatar-preset" data-avatar="lion.jpg" role="radio" aria-checked="false"><img src="/static/icons/avatar/lion.jpg" alt="" loading="lazy" /></button>
|
||||||
|
<button type="button" class="profile-avatar-preset" data-avatar="ours.jpg" role="radio" aria-checked="false"><img src="/static/icons/avatar/ours.jpg" alt="" loading="lazy" /></button>
|
||||||
|
<button type="button" class="profile-avatar-preset" data-avatar="penda.jpg" role="radio" aria-checked="false"><img src="/static/icons/avatar/penda.jpg" alt="" loading="lazy" /></button>
|
||||||
|
<button type="button" class="profile-avatar-preset" data-avatar="pingouin.jpg" role="radio" aria-checked="false"><img src="/static/icons/avatar/pingouin.jpg" alt="" loading="lazy" /></button>
|
||||||
|
<button type="button" class="profile-avatar-preset" data-avatar="raton.jpg" role="radio" aria-checked="false"><img src="/static/icons/avatar/raton.jpg" alt="" loading="lazy" /></button>
|
||||||
|
<button type="button" class="profile-avatar-preset" data-avatar="tigre.jpg" role="radio" aria-checked="false"><img src="/static/icons/avatar/tigre.jpg" alt="" loading="lazy" /></button>
|
||||||
|
</div>
|
||||||
<p class="profile-avatar-error hidden" id="profile-avatar-error" role="alert"></p>
|
<p class="profile-avatar-error hidden" id="profile-avatar-error" role="alert"></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="profile-field">
|
<div class="profile-field">
|
||||||
|
|||||||
@@ -2534,6 +2534,7 @@ function initProfile() {
|
|||||||
var overlayBtn = document.getElementById('profile-avatar-overlay');
|
var overlayBtn = document.getElementById('profile-avatar-overlay');
|
||||||
var previewBox = document.getElementById('profile-avatar-preview');
|
var previewBox = document.getElementById('profile-avatar-preview');
|
||||||
var removeBtn = document.getElementById('profile-avatar-remove');
|
var removeBtn = document.getElementById('profile-avatar-remove');
|
||||||
|
var presetsBox = document.getElementById('profile-avatar-presets');
|
||||||
if (!avatarField || !avatarInput || !chooseBtn) return;
|
if (!avatarField || !avatarInput || !chooseBtn) return;
|
||||||
|
|
||||||
// The avatar only exists on a real account — hide it when auth is off.
|
// The avatar only exists on a real account — hide it when auth is off.
|
||||||
@@ -2554,6 +2555,59 @@ function initProfile() {
|
|||||||
})
|
})
|
||||||
.catch(function () { /* non-bloquant */ });
|
.catch(function () { /* non-bloquant */ });
|
||||||
|
|
||||||
|
// ── Preset avatars (#117) ────────────────────────────────────────
|
||||||
|
var PRESET_STORAGE_KEY = 'obsigate-avatar-preset';
|
||||||
|
function markActivePreset(name) {
|
||||||
|
if (!presetsBox) return;
|
||||||
|
presetsBox.querySelectorAll('.profile-avatar-preset').forEach(function (btn) {
|
||||||
|
var on = !!name && btn.getAttribute('data-avatar') === name;
|
||||||
|
btn.classList.toggle('active', on);
|
||||||
|
btn.setAttribute('aria-checked', on ? 'true' : 'false');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function clearActivePreset() {
|
||||||
|
try { localStorage.removeItem(PRESET_STORAGE_KEY); } catch (e) { /* ignore */ }
|
||||||
|
markActivePreset(null);
|
||||||
|
}
|
||||||
|
function rememberActivePreset(name) {
|
||||||
|
try { localStorage.setItem(PRESET_STORAGE_KEY, name); } catch (e) { /* ignore */ }
|
||||||
|
markActivePreset(name);
|
||||||
|
}
|
||||||
|
var savedPreset = null;
|
||||||
|
try { savedPreset = localStorage.getItem(PRESET_STORAGE_KEY); } catch (e) { /* ignore */ }
|
||||||
|
markActivePreset(savedPreset);
|
||||||
|
|
||||||
|
if (presetsBox) {
|
||||||
|
presetsBox.querySelectorAll('.profile-avatar-preset').forEach(function (btn) {
|
||||||
|
btn.addEventListener('click', async function () {
|
||||||
|
var preset = btn.getAttribute('data-avatar');
|
||||||
|
if (!preset) return;
|
||||||
|
_profileAvatarError(null);
|
||||||
|
var all = presetsBox.querySelectorAll('.profile-avatar-preset');
|
||||||
|
all.forEach(function (b) { b.disabled = true; });
|
||||||
|
try {
|
||||||
|
// Load the bundled image, then reuse the upload pipeline (center-crop
|
||||||
|
// + 256 px JPEG) so it stores exactly like an imported picture.
|
||||||
|
var res = await fetch('/static/icons/avatar/' + encodeURIComponent(preset), { credentials: 'include' });
|
||||||
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||||
|
var blob = await res.blob();
|
||||||
|
var dataUrl = await _resizeAvatarFile(blob);
|
||||||
|
var user = await _patchProfileAvatar(dataUrl);
|
||||||
|
AuthManager.updateCachedUser(user);
|
||||||
|
_renderProfileAvatar(user.avatar || dataUrl);
|
||||||
|
AuthManager.renderUserSection();
|
||||||
|
rememberActivePreset(preset);
|
||||||
|
showToast(t('config.avatar_updated'), 'success');
|
||||||
|
} catch (err) {
|
||||||
|
_profileAvatarError('config.avatar_upload_failed');
|
||||||
|
console.error('Avatar preset failed:', err);
|
||||||
|
} finally {
|
||||||
|
all.forEach(function (b) { b.disabled = false; });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function openPicker() {
|
function openPicker() {
|
||||||
_profileAvatarError(null);
|
_profileAvatarError(null);
|
||||||
avatarInput.click();
|
avatarInput.click();
|
||||||
@@ -2587,6 +2641,7 @@ function initProfile() {
|
|||||||
AuthManager.updateCachedUser(user);
|
AuthManager.updateCachedUser(user);
|
||||||
_renderProfileAvatar(user.avatar || dataUrl);
|
_renderProfileAvatar(user.avatar || dataUrl);
|
||||||
AuthManager.renderUserSection();
|
AuthManager.renderUserSection();
|
||||||
|
clearActivePreset(); // an imported photo is not a preset
|
||||||
showToast(t('config.avatar_updated'), 'success');
|
showToast(t('config.avatar_updated'), 'success');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
_profileAvatarError('config.avatar_upload_failed');
|
_profileAvatarError('config.avatar_upload_failed');
|
||||||
@@ -2605,6 +2660,7 @@ function initProfile() {
|
|||||||
AuthManager.updateCachedUser(user);
|
AuthManager.updateCachedUser(user);
|
||||||
_renderProfileAvatar(null);
|
_renderProfileAvatar(null);
|
||||||
AuthManager.renderUserSection();
|
AuthManager.renderUserSection();
|
||||||
|
clearActivePreset();
|
||||||
showToast(t('config.avatar_removed'), 'success');
|
showToast(t('config.avatar_removed'), 'success');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
_profileAvatarError('config.avatar_upload_failed');
|
_profileAvatarError('config.avatar_upload_failed');
|
||||||
|
|||||||
@@ -598,6 +598,17 @@ function applyTheme(themeKey, mode) {
|
|||||||
// Notify other components of theme change
|
// Notify other components of theme change
|
||||||
try {
|
try {
|
||||||
root.setAttribute('data-theme', mode);
|
root.setAttribute('data-theme', mode);
|
||||||
|
// Syntax highlighting follows the light/dark mode (BUG-078): the theme
|
||||||
|
// *key* is not a mode, so toggling the sheets by key used to disable both
|
||||||
|
// and strip every code block of its colours. Sepia/high-contrast reuse the
|
||||||
|
// light palette.
|
||||||
|
var darkSheet = document.getElementById('hljs-theme-dark');
|
||||||
|
var lightSheet = document.getElementById('hljs-theme-light');
|
||||||
|
if (darkSheet && lightSheet) {
|
||||||
|
var isDark = mode === 'dark';
|
||||||
|
darkSheet.disabled = !isDark;
|
||||||
|
lightSheet.disabled = isDark;
|
||||||
|
}
|
||||||
document.dispatchEvent(new CustomEvent('themechange', { detail: { theme: themeKey, mode: mode } }));
|
document.dispatchEvent(new CustomEvent('themechange', { detail: { theme: themeKey, mode: mode } }));
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,14 +137,26 @@ export const RightSidebarManager = {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Theme
|
// Theme
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
const _THEME_MODES = ["dark", "light", "high-contrast", "sepia"];
|
||||||
|
|
||||||
export function initTheme() {
|
export function initTheme() {
|
||||||
const saved = localStorage.getItem("obsigate-theme") || "dark";
|
// The theme engine (themes.js) owns the CSS variables and the theme *key*;
|
||||||
applyTheme(saved);
|
// the <html data-theme> attribute must carry the *mode* (dark/light/…). Read
|
||||||
|
// the persisted mode here so the first paint and the highlight.js stylesheet
|
||||||
|
// are right before the async theme init runs (BUG-078).
|
||||||
|
let mode = "dark";
|
||||||
|
try { mode = localStorage.getItem("obsigate-theme-mode") || "dark"; } catch (e) { /* ignore */ }
|
||||||
|
applyTheme(mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyTheme(theme) {
|
export function applyTheme(theme) {
|
||||||
document.documentElement.setAttribute("data-theme", theme);
|
let mode = theme;
|
||||||
localStorage.setItem("obsigate-theme", theme);
|
if (_THEME_MODES.indexOf(mode) === -1) {
|
||||||
|
// Callers may pass a theme key (legacy): fall back to the persisted mode.
|
||||||
|
try { mode = localStorage.getItem("obsigate-theme-mode") || "dark"; } catch (e) { mode = "dark"; }
|
||||||
|
}
|
||||||
|
const isDark = mode === "dark";
|
||||||
|
document.documentElement.setAttribute("data-theme", mode);
|
||||||
|
|
||||||
// Update theme button icon and label
|
// Update theme button icon and label
|
||||||
const themeBtn = document.getElementById("theme-toggle");
|
const themeBtn = document.getElementById("theme-toggle");
|
||||||
@@ -152,23 +164,23 @@ export function applyTheme(theme) {
|
|||||||
if (themeBtn && themeLabel) {
|
if (themeBtn && themeLabel) {
|
||||||
const icon = themeBtn.querySelector("i");
|
const icon = themeBtn.querySelector("i");
|
||||||
if (icon) {
|
if (icon) {
|
||||||
icon.setAttribute("data-lucide", theme === "dark" ? "moon" : "sun");
|
icon.setAttribute("data-lucide", isDark ? "moon" : "sun");
|
||||||
}
|
}
|
||||||
themeLabel.textContent = theme === "dark" ? t('theme.dark') : t('theme.light');
|
themeLabel.textContent = isDark ? t('theme.dark') : t('theme.light');
|
||||||
safeCreateIcons();
|
safeCreateIcons();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Swap highlight.js theme
|
// Swap highlight.js theme — keyed on the mode, not the theme key (BUG-078).
|
||||||
const darkSheet = document.getElementById("hljs-theme-dark");
|
const darkSheet = document.getElementById("hljs-theme-dark");
|
||||||
const lightSheet = document.getElementById("hljs-theme-light");
|
const lightSheet = document.getElementById("hljs-theme-light");
|
||||||
if (darkSheet && lightSheet) {
|
if (darkSheet && lightSheet) {
|
||||||
darkSheet.disabled = theme !== "dark";
|
darkSheet.disabled = !isDark;
|
||||||
lightSheet.disabled = theme !== "light";
|
lightSheet.disabled = isDark;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update Mermaid theme for newly rendered diagrams
|
// Update Mermaid theme for newly rendered diagrams
|
||||||
import('./mermaid-viewer.js').then(function(m) {
|
import('./mermaid-viewer.js').then(function(m) {
|
||||||
m.updateMermaidTheme(theme === 'dark');
|
m.updateMermaidTheme(isDark);
|
||||||
}).catch(function() {});
|
}).catch(function() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1418,7 +1418,12 @@ export function renderFile(data) {
|
|||||||
// Assemble
|
// Assemble
|
||||||
area.innerHTML = "";
|
area.innerHTML = "";
|
||||||
area.appendChild(breadcrumb);
|
area.appendChild(breadcrumb);
|
||||||
area.appendChild(el("div", { class: "file-header" }, [el("div", { class: "file-title" }, [document.createTextNode(data.title)]), tagsDiv, el("div", { class: "file-actions" }, fileActions)]));
|
area.appendChild(el("div", { class: "file-header" }, [el("div", { class: "file-title" }, [document.createTextNode(data.title)]), tagsDiv]));
|
||||||
|
// #115 — the action bar is a direct child of the scroll container (wrapped in
|
||||||
|
// `.file-toolbar`) so it can stay pinned while long documents scroll; nested
|
||||||
|
// inside `.file-header` it would stop sticking as soon as the header left the
|
||||||
|
// viewport.
|
||||||
|
area.appendChild(el("div", { class: "file-toolbar" }, [el("div", { class: "file-actions" }, fileActions)]));
|
||||||
if (fmSection) area.appendChild(fmSection);
|
if (fmSection) area.appendChild(fmSection);
|
||||||
area.appendChild(mdDiv);
|
area.appendChild(mdDiv);
|
||||||
area.appendChild(rawDiv);
|
area.appendChild(rawDiv);
|
||||||
|
|||||||
@@ -386,6 +386,7 @@
|
|||||||
"config.key_deleted": "Key deleted:",
|
"config.key_deleted": "Key deleted:",
|
||||||
"config.avatar_label": "Profile picture",
|
"config.avatar_label": "Profile picture",
|
||||||
"config.avatar_hint": "PNG, JPG or WEBP — square image cropped and resized to 256 px. Shown in the sidebar.",
|
"config.avatar_hint": "PNG, JPG or WEBP — square image cropped and resized to 256 px. Shown in the sidebar.",
|
||||||
|
"config.avatar_presets_label": "Or pick a preset avatar",
|
||||||
"config.avatar_choose": "Choose an image",
|
"config.avatar_choose": "Choose an image",
|
||||||
"config.avatar_remove": "Remove picture",
|
"config.avatar_remove": "Remove picture",
|
||||||
"config.avatar_updated": "Profile picture updated",
|
"config.avatar_updated": "Profile picture updated",
|
||||||
|
|||||||
@@ -386,6 +386,7 @@
|
|||||||
"config.key_deleted": "Clé supprimée :",
|
"config.key_deleted": "Clé supprimée :",
|
||||||
"config.avatar_label": "Photo de profil",
|
"config.avatar_label": "Photo de profil",
|
||||||
"config.avatar_hint": "PNG, JPG ou WEBP — image carrée recadrée et réduite à 256 px. Apparaît dans la barre latérale.",
|
"config.avatar_hint": "PNG, JPG ou WEBP — image carrée recadrée et réduite à 256 px. Apparaît dans la barre latérale.",
|
||||||
|
"config.avatar_presets_label": "Ou choisissez un avatar prédéfini",
|
||||||
"config.avatar_choose": "Choisir une image",
|
"config.avatar_choose": "Choisir une image",
|
||||||
"config.avatar_remove": "Supprimer la photo",
|
"config.avatar_remove": "Supprimer la photo",
|
||||||
"config.avatar_updated": "Photo de profil mise à jour",
|
"config.avatar_updated": "Photo de profil mise à jour",
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<link rel="stylesheet" href="/static/style.css?v=2">
|
<link rel="stylesheet" href="/static/style.css?v=3">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" id="hljs-theme-dark">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" id="hljs-theme-dark">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css" id="hljs-theme-light" disabled>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css" id="hljs-theme-light" disabled>
|
||||||
@@ -871,8 +871,13 @@ const RightSidebarManager = {
|
|||||||
group.forEach(function (b) { actionsDiv.appendChild(b); });
|
group.forEach(function (b) { actionsDiv.appendChild(b); });
|
||||||
});
|
});
|
||||||
|
|
||||||
header.appendChild(actionsDiv);
|
|
||||||
area.appendChild(header);
|
area.appendChild(header);
|
||||||
|
// #115 — pinned action bar (direct child of the scroll container) so it
|
||||||
|
// stays visible while long documents scroll, as in the main viewer.
|
||||||
|
const toolbar = document.createElement("div");
|
||||||
|
toolbar.className = "file-toolbar";
|
||||||
|
toolbar.appendChild(actionsDiv);
|
||||||
|
area.appendChild(toolbar);
|
||||||
|
|
||||||
// Frontmatter — Accent Card
|
// Frontmatter — Accent Card
|
||||||
if (data.frontmatter && Object.keys(data.frontmatter).length > 0) {
|
if (data.frontmatter && Object.keys(data.frontmatter).length > 0) {
|
||||||
|
|||||||
@@ -1767,7 +1767,22 @@ select {
|
|||||||
|
|
||||||
/* File header */
|
/* File header */
|
||||||
.file-header {
|
.file-header {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
/* #115 — sticky document action bar: stays visible while long content scrolls.
|
||||||
|
A direct child of `.content-area` (the scroll container), opaque so the text
|
||||||
|
scrolling underneath never bleeds through. */
|
||||||
|
.file-toolbar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 30;
|
||||||
|
margin: 0 0 16px;
|
||||||
|
padding: 8px 0;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.file-toolbar .file-actions {
|
||||||
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
.file-title {
|
.file-title {
|
||||||
font-family: "JetBrains Mono", monospace;
|
font-family: "JetBrains Mono", monospace;
|
||||||
@@ -2670,6 +2685,32 @@ select {
|
|||||||
.profile-avatar-remove-btn { background: var(--danger-bg) !important; color: var(--danger) !important; border-color: var(--danger) !important; }
|
.profile-avatar-remove-btn { background: var(--danger-bg) !important; color: var(--danger) !important; border-color: var(--danger) !important; }
|
||||||
.profile-avatar-error { font-size: 0.7rem; color: var(--danger); margin-top: 8px; }
|
.profile-avatar-error { font-size: 0.7rem; color: var(--danger); margin-top: 8px; }
|
||||||
.profile-avatar-error.hidden { display: none; }
|
.profile-avatar-error.hidden { display: none; }
|
||||||
|
/* #117 — preset avatar gallery */
|
||||||
|
.profile-avatar-presets {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
max-width: 420px;
|
||||||
|
}
|
||||||
|
.profile-avatar-preset {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
aspect-ratio: 1;
|
||||||
|
transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.1s ease;
|
||||||
|
}
|
||||||
|
.profile-avatar-preset img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||||
|
.profile-avatar-preset:hover { border-color: var(--accent); transform: translateY(-1px); }
|
||||||
|
.profile-avatar-preset.active {
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
|
||||||
|
}
|
||||||
|
.profile-avatar-preset:disabled { opacity: 0.6; cursor: progress; }
|
||||||
.config-save-btn {
|
.config-save-btn {
|
||||||
padding: 6px 18px; font-size: 0.75rem; font-weight: 600;
|
padding: 6px 18px; font-size: 0.75rem; font-weight: 600;
|
||||||
background: var(--accent); color: #fff;
|
background: var(--accent); color: #fff;
|
||||||
@@ -4920,6 +4961,11 @@ body.resizing-v {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
/* Preset avatars: 4 per row on narrow screens (44px touch targets). */
|
||||||
|
#config-modal .profile-avatar-presets {
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
#config-modal .profile-form {
|
#config-modal .profile-form {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
@@ -11853,6 +11899,7 @@ body.reading-mode .main-body {
|
|||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
body.reading-mode .breadcrumb,
|
body.reading-mode .breadcrumb,
|
||||||
|
body.reading-mode .file-toolbar,
|
||||||
body.reading-mode .file-actions {
|
body.reading-mode .file-actions {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
* cache or Cloudflare does NOT clear the Service Worker Cache Storage, which is
|
* cache or Cloudflare does NOT clear the Service Worker Cache Storage, which is
|
||||||
* a separate store. Bumping SW_VERSION invalidates it on every release.
|
* a separate store. Bumping SW_VERSION invalidates it on every release.
|
||||||
*/
|
*/
|
||||||
const SW_VERSION = 'v25';
|
const SW_VERSION = 'v26';
|
||||||
const CODE_CACHE = `obsigate-code-${SW_VERSION}`;
|
const CODE_CACHE = `obsigate-code-${SW_VERSION}`;
|
||||||
const RUNTIME_CACHE = `obsigate-runtime-${SW_VERSION}`;
|
const RUNTIME_CACHE = `obsigate-runtime-${SW_VERSION}`;
|
||||||
const API_CACHE = `obsigate-api-${SW_VERSION}`;
|
const API_CACHE = `obsigate-api-${SW_VERSION}`;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsigate",
|
"name": "obsigate",
|
||||||
"version": "2.24.0",
|
"version": "2.25.0",
|
||||||
"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": {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { strict as assert } from "node:assert";
|
import { strict as assert } from "node:assert";
|
||||||
import { readFileSync } from "node:fs";
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
@@ -136,11 +136,46 @@ test("style.css — preview circle and sidebar image rules exist", () => {
|
|||||||
assert.match(css, /\.sidebar-user-avatar-img\s*\{/, "no .sidebar-user-avatar-img rule");
|
assert.match(css, /\.sidebar-user-avatar-img\s*\{/, "no .sidebar-user-avatar-img rule");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── #117: preset avatar gallery ─────────────────────────────────────────────
|
||||||
|
test("index.html — #cfg-profile exposes 12 bundled preset avatars", () => {
|
||||||
|
const section = indexHtml.match(/<section[^>]*id="cfg-profile"[\s\S]*?<\/section>/);
|
||||||
|
assert.ok(section, "#cfg-profile section not found");
|
||||||
|
const html = section[0];
|
||||||
|
assert.match(html, /id="profile-avatar-presets"/, "preset gallery container missing");
|
||||||
|
const buttons = [...html.matchAll(/class="profile-avatar-preset"[^>]*data-avatar="([^"]+)"/g)];
|
||||||
|
assert.equal(buttons.length, 12, `expected 12 preset avatars, found ${buttons.length}`);
|
||||||
|
const names = buttons.map((m) => m[1]);
|
||||||
|
assert.equal(new Set(names).size, 12, "preset avatar names must be unique");
|
||||||
|
for (const name of names) {
|
||||||
|
const rel = path.join(ROOT, "frontend", "icons", "avatar", name);
|
||||||
|
assert.ok(existsSync(rel), `missing avatar file: ${rel}`);
|
||||||
|
assert.ok(html.includes(`/static/icons/avatar/${name}`),
|
||||||
|
`#${name} must be served from /static/icons/avatar/`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("config.js — preset avatars reuse the upload pipeline", () => {
|
||||||
|
assert.match(configJs, /profile-avatar-presets/, "preset container lookup missing");
|
||||||
|
assert.match(configJs, /\.profile-avatar-preset/, "preset button selector missing");
|
||||||
|
assert.match(configJs, /\/static\/icons\/avatar\//, "preset image base URL missing");
|
||||||
|
assert.match(configJs, /_resizeAvatarFile\(blob\)/, "preset must go through the resize pipeline");
|
||||||
|
assert.match(configJs, /_patchProfileAvatar\(dataUrl\)/, "preset must be persisted via PATCH");
|
||||||
|
assert.match(configJs, /obsigate-avatar-preset/, "active preset marker missing");
|
||||||
|
assert.match(configJs, /markActivePreset/, "active preset highlight missing");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("style.css — preset avatar rules exist", () => {
|
||||||
|
assert.match(css, /\.profile-avatar-presets\s*\{/, "no .profile-avatar-presets rule");
|
||||||
|
assert.match(css, /\.profile-avatar-preset\s*\{/, "no .profile-avatar-preset rule");
|
||||||
|
assert.match(css, /\.profile-avatar-preset\.active\s*\{/, "no active preset rule");
|
||||||
|
});
|
||||||
|
|
||||||
// ── #113c: i18n + backend ───────────────────────────────────────────────────
|
// ── #113c: i18n + backend ───────────────────────────────────────────────────
|
||||||
test("i18n — avatar keys exist in FR and EN", () => {
|
test("i18n — avatar keys exist in FR and EN", () => {
|
||||||
const keys = [
|
const keys = [
|
||||||
"config.avatar_label",
|
"config.avatar_label",
|
||||||
"config.avatar_hint",
|
"config.avatar_hint",
|
||||||
|
"config.avatar_presets_label",
|
||||||
"config.avatar_choose",
|
"config.avatar_choose",
|
||||||
"config.avatar_remove",
|
"config.avatar_remove",
|
||||||
"config.avatar_updated",
|
"config.avatar_updated",
|
||||||
|
|||||||
@@ -111,6 +111,30 @@ test("style.css — .action-sep rule exists", () => {
|
|||||||
assert.match(css, /\.file-actions \.action-sep\s*\{[^}]*background:[^;]*;/);
|
assert.match(css, /\.file-actions \.action-sep\s*\{[^}]*background:[^;]*;/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── #115 : barre d'outils collante au défilement ─────────────────────────
|
||||||
|
test("viewer.js — action bar wrapped in a sticky .file-toolbar", () => {
|
||||||
|
assert.match(viewer, /class:\s*"file-toolbar"[\s\S]{0,120}file-actions/,
|
||||||
|
"the document action bar must live in a .file-toolbar wrapper");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("popout.html — action bar wrapped in a .file-toolbar", () => {
|
||||||
|
assert.match(popout, /className\s*=\s*"file-toolbar"/,
|
||||||
|
"popout action bar must live in a .file-toolbar wrapper");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("style.css — .file-toolbar is pinned to the top while scrolling", () => {
|
||||||
|
const m = css.match(/\.file-toolbar\s*\{([\s\S]*?)\}/);
|
||||||
|
assert.ok(m, ".file-toolbar rule missing");
|
||||||
|
assert.match(m[1], /position:\s*sticky/, ".file-toolbar must be sticky");
|
||||||
|
assert.match(m[1], /top:\s*0/, ".file-toolbar must pin to the top");
|
||||||
|
assert.match(m[1], /background:/, ".file-toolbar needs an opaque background");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("style.css — reading mode hides the sticky toolbar", () => {
|
||||||
|
assert.match(css, /body\.reading-mode\s+\.file-toolbar\s*[,{]/,
|
||||||
|
"reading mode must hide .file-toolbar");
|
||||||
|
});
|
||||||
|
|
||||||
if (process.exitCode) {
|
if (process.exitCode) {
|
||||||
console.error("\nToolbar order tests FAILED");
|
console.error("\nToolbar order tests FAILED");
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -284,6 +284,28 @@ function testSidebarUserSection() {
|
|||||||
console.log(' ✓ header trimmed + sidebar account section (#112)');
|
console.log(' ✓ header trimmed + sidebar account section (#112)');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Test syntax highlighting follows the theme mode (BUG-078) ──────────────
|
||||||
|
function testSyntaxHighlightTheme() {
|
||||||
|
const themes = readFileSync(join(JS_DIR, 'themes.js'), 'utf-8');
|
||||||
|
const ui = readFileSync(join(JS_DIR, 'ui.js'), 'utf-8');
|
||||||
|
|
||||||
|
// themes.js must swap the highlight.js stylesheets based on the *mode*.
|
||||||
|
const applyTheme = themes.match(/function applyTheme\(themeKey, mode\)\s*\{([\s\S]*?)\n\}/);
|
||||||
|
assert.ok(applyTheme, 'themes.js applyTheme not found');
|
||||||
|
assert.match(applyTheme[1], /hljs-theme-dark/, 'applyTheme must toggle the dark hljs sheet');
|
||||||
|
assert.match(applyTheme[1], /hljs-theme-light/, 'applyTheme must toggle the light hljs sheet');
|
||||||
|
assert.match(applyTheme[1], /mode === 'dark'|mode === "dark"/, 'sheet choice must key on the mode');
|
||||||
|
|
||||||
|
// ui.js must resolve the persisted *mode*, never treat the theme key as one.
|
||||||
|
assert.match(ui, /obsigate-theme-mode/, 'ui.js must read the persisted theme mode');
|
||||||
|
assert.doesNotMatch(
|
||||||
|
ui,
|
||||||
|
/darkSheet\.disabled\s*=\s*theme !== "dark"[\s\S]{0,120}lightSheet\.disabled\s*=\s*theme !== "light"/,
|
||||||
|
'ui.js must not compare the theme key to "dark"/"light" (BUG-078 regression)',
|
||||||
|
);
|
||||||
|
console.log(' ✓ syntax highlighting follows the theme mode (BUG-078)');
|
||||||
|
}
|
||||||
|
|
||||||
// ── Run all tests ──────────────────────────────────────────────────────────
|
// ── Run all tests ──────────────────────────────────────────────────────────
|
||||||
async function main() {
|
async function main() {
|
||||||
let passed = 0, failed = 0;
|
let passed = 0, failed = 0;
|
||||||
@@ -300,6 +322,7 @@ async function main() {
|
|||||||
['ai-fab module exports', testAIFabModuleExports],
|
['ai-fab module exports', testAIFabModuleExports],
|
||||||
['config TOC icons', testConfigTocIcons],
|
['config TOC icons', testConfigTocIcons],
|
||||||
['sidebar user section', testSidebarUserSection],
|
['sidebar user section', testSidebarUserSection],
|
||||||
|
['syntax highlight theme', testSyntaxHighlightTheme],
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const [name, fn] of tests) {
|
for (const [name, fn] of tests) {
|
||||||
|
|||||||