- ROADMAP: ne garde que le travail a venir + index compact du complete (995 -> ~155 lignes); detail deplace vers docs/features/ et docs/archive/ - docs/features/: fiches detaillees #74, #75, #76, #77, #78, #79 - docs/archive/COMPLETED_v1-v2.md: detail des items courts livres - CHANGELOG: alignement sur les tags (2.0.0 date, 2.2.0/2.2.1 ajoutes, Unreleased = travail #79 post-2.2.1) - AGENTS.md + docs/DELIVERY_WORKFLOW.md: methode de livraison unique (Definition of Done) referencee par ROADMAP, CONTRIBUTING, ISSUES_TODOLIST
16 KiB
#78 — Éditeur Excalidraw — Ouverture et édition de fichiers .excalidraw
Statut : ✅ Terminé (2026-09-10 — éditeur iframe complet, détection, création, autosave, support
.excalidraw.md, B5 extraction texte pour la recherche, C8 création via menu contextuel, F3 E2Etests/e2e/excalidraw.spec.js, doc H1-H3. F2 non retenu. BUG-002 corrigé) Effort : 3-4 jours | Impact : 🟡 Références : Roadmap · Changelog — 2.2.0
-
Description : Prise en charge native des fichiers
.excalidrawdans ObsiGate avec un éditeur visuel complet intégré. L'utilisateur peut ouvrir un fichier.excalidrawdepuis l'arborescence et obtenir l'éditeur de diagrammes Excalidraw directement dans ObsiGate — dessiner, modifier, sauvegarder, comme dans l'app Excalidraw standalone, mais intégré au flux de travail du vault Obsidian. -
Pourquoi c'est important : Excalidraw est devenu le standard de fait pour les diagrammes et croquis dans l'écosystème Obsidian (plugin communautaire avec 1M+ téléchargements). Les utilisateurs créent des
.excalidrawdans leur vault et s'attendent à pouvoir les visualiser et éditer. Actuellement ObsiGate traite ces fichiers comme du JSON brut — illisible. Avec l'éditeur intégré, ObsiGate devient un viewer/éditeur Excalidraw à part entière, supprimant le besoin d'ouvrir Obsidian Desktop ou l'app web Excalidraw séparément. -
Fonctionnement général :
- L'utilisateur clique sur un fichier
.excalidrawdans l'arborescence → ObsiGate détecte l'extension et le type de contenu (type: "excalidraw"dans le JSON) - Au lieu du viewer markdown ou JSON brut, une iframe sandbox charge l'éditeur Excalidraw avec les données du fichier
- L'utilisateur peut dessiner, ajouter des formes, du texte, des flèches, des images — l'expérience Excalidraw complète
- Les modifications sont sauvegardées automatiquement (Ctrl+S ou auto-save) via
postMessage→ le parent écrit dans le fichier via l'API ObsiGate - L'éditeur respecte le thème sombre/clair d'ObsiGate
- L'utilisateur clique sur un fichier
-
Architecture :
ObsiGate SPA (content-area) └── <iframe sandbox="allow-scripts allow-same-origin"> └── /frontend/excalidraw-editor.html ├── import * as ExcalidrawLib from "esm.sh/@excalidraw/excalidraw" ├── React + ReactDOM (fournis par Excalidraw) ├── Écoute postMessage("init", {data, theme}) └── Poste postMessage("save", {data}) au parent -
Choix technique — Pourquoi une iframe plutôt qu'une intégration directe ?
- Isolation : Excalidraw est un composant React avec son propre DOM virtuel, ses propres polices, et des styles CSS globaux. L'iframe empêche les conflits CSS avec ObsiGate (variables CSS, polices, z-index des modales).
- Sandbox : L'iframe isole le code d'Excalidraw — une erreur dans l'éditeur ne crashe pas l'application principale.
- Chargement lazy : Excalidraw pèse ~2 Mo minifié + React ~40 Ko. L'iframe n'est chargée QUE quand l'utilisateur ouvre un fichier
.excalidraw. Pas d'impact sur le temps de chargement initial. - Communication standard :
postMessageest une API web native, simple et sécurisée. L'iframe n'a pas accès au DOM parent, seulement au canal de messages. - CSS indépendant : Le thème sombre/clair est passé comme paramètre → l'iframe applique son propre thème sans toucher aux variables CSS d'ObsiGate.
-
Sous-tâches :
A. Fichier frontend/excalidraw-editor.html — Éditeur autonome (1.5 jour)
- A1. Structure HTML : Page minimale avec un
<div id="excalidraw-container">en plein écran. Pas de header ObsiGate — tout l'espace est pour le canvas. - A2. Import Excalidraw :
Version épinglée (
<script type="module"> import * as ExcalidrawLib from "https://esm.sh/@excalidraw/[email protected]"; window.ExcalidrawLib = ExcalidrawLib; </script>@0.18.0) pour la stabilité. Mise à jour manuelle testée. - A3. Configuration du chemin d'assets : Définir
window.EXCALIDRAW_ASSET_PATHpour pointer vers le CDN des fonts/polices d'Excalidraw (nécessaire pour le rendu des polices handwriting). - A4. Initialisation React (import map esm.sh → React 18.3.1 épinglé) : Excalidraw nécessite React + ReactDOM. Les importer depuis esm.sh également :
<script type="module"> import React from "https://esm.sh/react@18"; import ReactDOM from "https://esm.sh/react-dom@18"; window.React = React; window.ReactDOM = ReactDOM; </script> - A5. Rendu du composant : Monter
<ExcalidrawLib.Excalidraw>dans le conteneur avec lesinitialDatareçues. Configurer les callbacksonChangepour détecter les modifications. - A6. Barre d'outils minimaliste (dans l'iframe, superposée en haut à droite) :
- Bouton « 💾 Sauvegarder » → envoie les données au parent
- Badge « Modifié » (disparaît après sauvegarde)
- Indicateur de thème 🌙/☀️
- Optionnel : bouton « Export PNG » et « Export SVG » (natif Excalidraw)
- A7. Communication postMessage :
- Réception : écouter
message→ sitype === "init", chargerdata.elements+data.appState+data.filesdans l'état Excalidraw. Sitype === "theme", basculertheme(dark/light). - Émission :
postMessage({type: "save", data: {elements, appState, files}}, "*")quand l'utilisateur sauvegarde. - Émission :
postMessage({type: "ready"}, "*")au chargement pour signaler que l'iframe est prête. - Émission :
postMessage({type: "modified", dirty: true/false}, "*")pour l'indicateur de modification.
- Réception : écouter
- A8. Gestion des erreurs : Si les données sont invalides (JSON corrompu, pas un fichier Excalidraw), afficher un message d'erreur stylisé dans l'iframe.
B. Backend — Détection et API (0.5 jour)
- B1. Ajout à
SUPPORTED_EXTENSIONS: Ajouter.excalidrawdansbackend/indexer.py:56pour que les fichiers apparaissent dans l'arborescence et soient indexés. - B2. Icône : Ajouter
.excalidrawdansEXT_ICONS(frontend/js/utils.js) → icônepen-toolouedit-3(Lucide). - B3. Détection dans
api_file_view(): Dansbackend/main.py, pour les fichiers.excalidraw:- Lire le JSON
- Vérifier
data.get("type") === "excalidraw" - Retourner
is_excalidraw: true+ les données parsées (elements,appState,files) - Si le JSON est invalide ou n'est pas un fichier Excalidraw valide → fallback sur le viewer JSON standard
- B4. Endpoint de sauvegarde : Le endpoint existant
PUT /api/file/{vault}fonctionne déjà pour écrire du contenu. L'iframe envoie le JSON modifié via postMessage → le parent appelle l'API existante. Aucun nouvel endpoint nécessaire. - B5. Indexation du contenu texte (FAIT 2026-09) :
extract_excalidraw_indexable()extraitelement.textdes éléments (JSON pur et.excalidraw.mdcompressé) → recherche TF-IDF fonctionnelle. - B6. Contenu initial pour nouveaux fichiers : Définir le squelette JSON minimum pour un fichier
.excalidrawvide :Ce squelette est retourné par le backend quand on crée un fichier{"type":"excalidraw","version":2,"elements":[],"appState":{"viewBackgroundColor":"#ffffff"},"files":{}}.excalidraw(utilisé parPOST /api/file/{vault}).
C. Frontend — Intégration dans le viewer (1 jour)
- C1. Module
frontend/js/excalidraw-viewer.js(nouveau) : FonctionrenderExcalidraw(container, data, vault, path):- Crée une
<iframe>avecsrc="/frontend/excalidraw-editor.html"etsandbox="allow-scripts allow-same-origin" - Stocke une référence à l'iframe pour la communication
- Attend le message
readyde l'iframe - Envoie
postMessage({type: "init", data: {elements, appState, files}, theme})à l'iframe - Écoute les messages
save→ appellesaveFile(vault, path, JSON.stringify(data))via l'API existante - Écoute les messages
modified→ met à jour l'indicateur dans la barre d'onglets - Gère le thème : écoute
themeChanged→ envoiepostMessage({type: "theme", theme})à l'iframe
- Crée une
- C2. Dispatch dans
viewer.js: DansrenderFileContent()ourenderFile():- Après la détection
data.is_json, ajouter une branche : sidata.is_excalidraw === true→ appelerrenderExcalidraw(container, data, vaultName, filePath) - Ne PAS passer par le viewer markdown standard
- Après la détection
- C3. Barre d'outils contextuelle : Dans la toolbar du viewer (celle avec Copier/Source/Éditer/PDF/pop-out) :
- Pour les fichiers
.excalidraw: remplacer « Éditer (Forge) » par « Ouvrir dans Excalidraw.com » (lien externe, nouvel onglet) - Garder « Télécharger » (.excalidraw) et « pop-out »
- Badge « Excalidraw » avec icône
pen-tool
- Pour les fichiers
- C4. Auto-save : Débounce 2 secondes après la dernière modification dans l'iframe → sauvegarde automatique silencieuse (comme l'éditeur markdown #29). L'iframe émet
modified→ le parent démarre un timer → au bout de 2s sans nouvelle modification →postMessage({type: "requestSave"})→ l'iframe répond avecsave→ le parent écrit via l'API. - C5. Raccourci Ctrl+S : L'iframe intercepte Ctrl+S → envoie
saveau parent → le parent sauvegarde → confirmation visuelle (toast « Excalidraw sauvegardé »). - C6. Compatibilité Split View (#75) : L'iframe s'affiche dans le content-area du panneau actif. Le
PaneTabManagergère le cache : quand on switch d'onglet, l'état de l'iframe est préservé (elle reste dans le DOM, juste masquée). Plusieurs iframes Excalidraw peuvent coexister dans différents panneaux. - C7. Création via la modale « Nouveau fichier » : Dans
frontend/js/ui.js, fonctionshowCreateFileModal():- Ajouter
<option value=".excalidraw">Excalidraw (.excalidraw)</option>dans le<select id="file-ext-select">(après.json) - Quand l'extension
.excalidrawest sélectionnée, le backend crée le fichier avec le squelette JSON minimum (B6) - Après création →
openFile(vault, path)→ le viewer détecteis_excalidraw: true→ l'iframe s'ouvre avec le canvas vierge - Fonctionne aussi via la palette de commandes
Ctrl+Alt+Space→ « Nouveau fichier » (actioncreate-fileexistante)
- Ajouter
- C8. Création via le menu contextuel (FAIT 2026-09 —
frontend/js/ui.js_createExcalidraw(), option « Nouveau diagramme Excalidraw » sur les répertoires, E2E couvert) : ouvre la modale avec.excalidrawpré-sélectionné.
D. CSS & Design (0.5 jour)
- D1. Styles de l'iframe dans ObsiGate : L'iframe occupe 100% du content-area (
width: 100%; height: 100%; border: none;). Aucun padding ni marge. - D2. Thème dark/light : L'iframe reçoit le thème courant → Excalidraw applique son thème interne (
theme="dark"outheme="light"). Les couleurs sont cohérentes avec ObsiGate grâce à la palette d'Excalidraw. - D3. Écran de chargement : Pendant le chargement de l'iframe (React + Excalidraw ~2 Mo), afficher un spinner « Chargement de l'éditeur Excalidraw... » dans le content-area. L'iframe envoie
ready→ le spinner disparaît. - D4. Responsive : L'iframe s'adapte à la largeur du panneau. En mode mobile (<768px), l'éditeur Excalidraw est utilisable (UI tactile native).
E. Gestion des conflits et edge cases (0.5 jour)
- E1. Fichier modifié à l'extérieur : Si le fichier est modifié par Syncthing/watcher pendant l'édition → détecter via le watcher → afficher un bandeau « Ce fichier a été modifié à l'extérieur. Recharger ? » avec boutons [Recharger] [Ignorer].
- E2. Plusieurs onglets : Deux onglets sur le même fichier
.excalidraw→ le second détecte que le fichier est déjà ouvert → focus l'onglet existant (comportement existant duTabManager#E4). - E3. Fichier vide ou nouveau : Couvert par C7/C8 — la création d'un
.excalidrawproduit un canvas vierge avec le squelette JSON minimum (B6). L'iframe gère nativement le caselements: []. - E4. Fichier corrompu (fallback viewer JSON + tests
test_invalid_json_excalidraw_fallback/test_json_without_excalidraw_type) : Si le JSON ne contient pastype: "excalidraw"ou est invalide → fallback sur le viewer JSON standard avec un message « Ce fichier .excalidraw semble corrompu ». - E5. Pop-out : Le bouton pop-out fonctionne — il ouvre l'éditeur dans une popup séparée avec sa propre iframe. Utile pour éditer sur un deuxième écran.
- E6. Annulation (Ctrl+Z) : Natif dans Excalidraw — l'historique d'annulation est géré par l'état interne de l'iframe. Pas besoin d'interaction avec le parent.
F. Tests (0.5 jour) — F1 ✅ (6 tests test_excalidraw.py)
- F1. Tests backend :
test_excalidraw_detection.py: fichier.excalidrawvalide →is_excalidraw: true, JSON invalide → fallback JSON, fichier sanstype: excalidraw→ fallbacktest_excalidraw_search.py: texte extrait des éléments → recherchable via TF-IDF
- F2. Tests frontend — ⚪ NON RETENU :
- Chargement de l'iframe avec des données de test
- Communication postMessage (init → ready → save)
- Changement de thème propagé à l'iframe
- F3. Tests E2E (Playwright) (FAIT 2026-09 —
tests/e2e/excalidraw.spec.js) :- Ouvrir un fichier
.excalidraw→ l'iframe se charge → le canvas Excalidraw est visible - Dessiner un rectangle → sauvegarder → recharger → le rectangle est toujours là
- Basculer thème sombre → l'iframe passe en dark mode
- Ouvrir un fichier
G. Points d'attention / Risques
- Taille du bundle : React + ReactDOM + Excalidraw ≈ 2.5 Mo minifié. Chargé depuis
esm.sh(CDN global, cache HTTP). L'impact n'est perceptible qu'à la première ouverture d'un.excalidraw. Solution : précharger l'iframe en arrière-plan (<link rel="prefetch">) après le chargement de l'app. - Performance React dans iframe : React dans une iframe fonctionne parfaitement — c'est un contexte JavaScript indépendant. Testé sur Chrome, Firefox, Safari, Edge.
- CORS et esm.sh : Les modules ESM depuis
esm.shsont servis avec les headers CORS appropriés. L'iframe est same-origin (/frontend/excalidraw-editor.html) donc pas de problème. - Mises à jour d'Excalidraw : La version est épinglée (
@0.18.0). Pour mettre à jour, changer le numéro dans le HTML + tester. Le format de données.excalidrawest stable (v2 depuis 2021). - Sécurité postMessage : Vérifier
event.origindans les deux sens. L'iframe n'accepte que les messages dewindow.parent. Le parent n'accepte que les messages de l'iframe connue. Pas de"*"en production. - Tauri Desktop (#77) : L'iframe se charge depuis le filesystem local (
tauri://localhost/frontend/excalidraw-editor.html). Les imports ESM depuisesm.shfonctionnent si le réseau est disponible. Pour le mode offline, bundler Excalidraw dans l'app desktop (à traiter dans #77, pas ici). - Pas d'édition collaborative : Cette implémentation est mono-utilisateur. La collaboration temps réel (#62) pourra être étendue aux fichiers
.excalidrawultérieurement via le même mécanisme Yjs.
H. Documentation utilisateur
- H1. Mettre à jour README : ajouter
.excalidrawdans les formats supportés (FR + EN) - H2. Ajouter dans le guide d'utilisation (Quick Help) : section « Diagrammes Excalidraw » (i18n FR/EN)
- H3. Note : « Les fichiers .excalidraw créés avec le plugin Obsidian Excalidraw sont compatibles »