Files
ObsiGate/docs/features/excalidraw.md
T
bruno ce23ab38f7
CI / lint (push) Successful in 57s
CI / security (push) Successful in 40s
CI / test (push) Successful in 1m13s
CI / build (push) Successful in 34s
CI / e2e (push) Successful in 10m33s
docs: restructurer le suivi et unifier la methode de livraison
- 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
2026-09-11 14:07:56 -04:00

153 lines
16 KiB
Markdown

# #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 E2E `tests/e2e/excalidraw.spec.js`, doc H1-H3. F2 non retenu. BUG-002 corrigé)
> **Effort :** 3-4 jours | **Impact :** 🟡
> **Références :** [Roadmap](../ROADMAP.md) · [Changelog — 2.2.0](../../CHANGELOG.md)
- **Description :** Prise en charge native des fichiers `.excalidraw` dans ObsiGate avec un éditeur visuel complet intégré. L'utilisateur peut ouvrir un fichier `.excalidraw` depuis 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 `.excalidraw` dans 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 `.excalidraw` dans 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
- **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** : `postMessage` est 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)
- [x] **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.
- [x] **A2. Import Excalidraw** :
```html
<script type="module">
import * as ExcalidrawLib from "https://esm.sh/@excalidraw/[email protected]";
window.ExcalidrawLib = ExcalidrawLib;
</script>
```
Version épinglée (`@0.18.0`) pour la stabilité. Mise à jour manuelle testée.
- [x] **A3. Configuration du chemin d'assets** : Définir `window.EXCALIDRAW_ASSET_PATH` pour pointer vers le CDN des fonts/polices d'Excalidraw (nécessaire pour le rendu des polices handwriting).
- [x] **A4. Initialisation React** (import map esm.sh → React 18.3.1 épinglé) : Excalidraw nécessite React + ReactDOM. Les importer depuis esm.sh également :
```html
<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>
```
- [x] **A5. Rendu du composant** : Monter `<ExcalidrawLib.Excalidraw>` dans le conteneur avec les `initialData` reçues. Configurer les callbacks `onChange` pour détecter les modifications.
- [x] **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)
- [x] **A7. Communication postMessage** :
- Réception : écouter `message` → si `type === "init"`, charger `data.elements` + `data.appState` + `data.files` dans l'état Excalidraw. Si `type === "theme"`, basculer `theme` (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.
- [x] **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)
- [x] **B1. Ajout à `SUPPORTED_EXTENSIONS`** : Ajouter `.excalidraw` dans `backend/indexer.py:56` pour que les fichiers apparaissent dans l'arborescence et soient indexés.
- [x] **B2. Icône** : Ajouter `.excalidraw` dans `EXT_ICONS` (`frontend/js/utils.js`) → icône `pen-tool` ou `edit-3` (Lucide).
- [x] **B3. Détection dans `api_file_view()`** : Dans `backend/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
- [x] **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.
- [x] **B5. Indexation du contenu texte** (FAIT 2026-09) : `extract_excalidraw_indexable()` extrait `element.text` des éléments (JSON pur et `.excalidraw.md` compressé) → recherche TF-IDF fonctionnelle.
- [x] **B6. Contenu initial pour nouveaux fichiers** : Définir le squelette JSON minimum pour un fichier `.excalidraw` vide :
```json
{"type":"excalidraw","version":2,"elements":[],"appState":{"viewBackgroundColor":"#ffffff"},"files":{}}
```
Ce squelette est retourné par le backend quand on crée un fichier `.excalidraw` (utilisé par `POST /api/file/{vault}`).
## C. Frontend — Intégration dans le viewer (1 jour)
- [x] **C1. Module `frontend/js/excalidraw-viewer.js`** (nouveau) : Fonction `renderExcalidraw(container, data, vault, path)` :
- Crée une `<iframe>` avec `src="/frontend/excalidraw-editor.html"` et `sandbox="allow-scripts allow-same-origin"`
- Stocke une référence à l'iframe pour la communication
- Attend le message `ready` de l'iframe
- Envoie `postMessage({type: "init", data: {elements, appState, files}, theme})` à l'iframe
- Écoute les messages `save` → appelle `saveFile(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` → envoie `postMessage({type: "theme", theme})` à l'iframe
- [x] **C2. Dispatch dans `viewer.js`** : Dans `renderFileContent()` ou `renderFile()` :
- Après la détection `data.is_json`, ajouter une branche : si `data.is_excalidraw === true` → appeler `renderExcalidraw(container, data, vaultName, filePath)`
- Ne PAS passer par le viewer markdown standard
- [x] **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`
- [x] **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 avec `save` → le parent écrit via l'API.
- [x] **C5. Raccourci Ctrl+S** : L'iframe intercepte Ctrl+S → envoie `save` au parent → le parent sauvegarde → confirmation visuelle (toast « Excalidraw sauvegardé »).
- [x] **C6. Compatibilité Split View (#75)** : L'iframe s'affiche dans le content-area du panneau actif. Le `PaneTabManager` gè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.
- [x] **C7. Création via la modale « Nouveau fichier »** : Dans `frontend/js/ui.js`, fonction `showCreateFileModal()` :
- Ajouter `<option value=".excalidraw">Excalidraw (.excalidraw)</option>` dans le `<select id="file-ext-select">` (après `.json`)
- Quand l'extension `.excalidraw` est 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étecte `is_excalidraw: true` → l'iframe s'ouvre avec le canvas vierge
- Fonctionne aussi via la palette de commandes `Ctrl+Alt+Space` → « Nouveau fichier » (action `create-file` existante)
- [x] **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 `.excalidraw` pré-sélectionné.
## D. CSS & Design (0.5 jour)
- [x] **D1. Styles de l'iframe dans ObsiGate** : L'iframe occupe 100% du content-area (`width: 100%; height: 100%; border: none;`). Aucun padding ni marge.
- [x] **D2. Thème dark/light** : L'iframe reçoit le thème courant → Excalidraw applique son thème interne (`theme="dark"` ou `theme="light"`). Les couleurs sont cohérentes avec ObsiGate grâce à la palette d'Excalidraw.
- [x] **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.
- [x] **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)
- [x] **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].
- [x] **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 du `TabManager` #E4).
- [x] **E3. Fichier vide ou nouveau** : Couvert par C7/C8 — la création d'un `.excalidraw` produit un canvas vierge avec le squelette JSON minimum (B6). L'iframe gère nativement le cas `elements: []`.
- [x] **E4. Fichier corrompu** (fallback viewer JSON + tests `test_invalid_json_excalidraw_fallback`/`test_json_without_excalidraw_type`) : Si le JSON ne contient pas `type: "excalidraw"` ou est invalide → fallback sur le viewer JSON standard avec un message « Ce fichier .excalidraw semble corrompu ».
- [x] **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.
- [x] **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)
- [x] **F1. Tests backend** :
- `test_excalidraw_detection.py` : fichier `.excalidraw` valide → `is_excalidraw: true`, JSON invalide → fallback JSON, fichier sans `type: excalidraw` → fallback
- `test_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
- [x] **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
## 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.sh` sont 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 `.excalidraw` est stable (v2 depuis 2021).
- **Sécurité postMessage** : Vérifier `event.origin` dans les deux sens. L'iframe n'accepte que les messages de `window.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 depuis `esm.sh` fonctionnent 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 `.excalidraw` ultérieurement via le même mécanisme Yjs.
## H. Documentation utilisateur
- [x] **H1.** Mettre à jour README : ajouter `.excalidraw` dans les formats supportés (FR + EN)
- [x] **H2.** Ajouter dans le guide d'utilisation (Quick Help) : section « Diagrammes Excalidraw » (i18n FR/EN)
- [x] **H3.** Note : « Les fichiers .excalidraw créés avec le plugin Obsidian Excalidraw sont compatibles »