feat(editeur): #93 edition inline — Editer/Forge remplacent la vue lecture
CI / lint (push) Successful in 1m26s
CI / security (push) Successful in 1m0s
CI / test (push) Successful in 3m17s
CI / build (push) Successful in 56s
CI / e2e (push) Successful in 10m47s

Le conteneur d'edition (#editor-container, CodeMirror ou iframe Forge) est deplace dans la zone de contenu du document (#content-area ou pane active) au lieu de l'overlay plein ecran : le mode edition remplace la vue lecture. L'overlay reste monte (transparent, pointer-events: none) car le ruban d'edition mobile y est ancre ; il ne sert plus que de repli quand la cible n'est pas le document affiche. Garde-fou dans renderFile() pour liberer proprement la session (destroy CodeMirror/Yjs, retrait de l'iframe Forge) ; forge-close passe par closeEditor() ; cache d'onglet invalide au retour en lecture.

Assistant IA : app_context.editing annonce le document en cours d'edition (frontend bookslm.js + backend bookslm.py), et chaque ecriture d'outil (edit_file, append_to_file, create_file, restore_backup) recharge le document affiche (obsigate:file-written) — tampon CodeMirror remplace avec auto-save neutralisee, parent-reload pour l'iframe Forge, re-rendu de la vue lecture sinon.

Tests : tests/frontend/editor-inline.test.mjs (19), tests/test_bookslm.py::TestGeneralPrompt (3 nouveaux). Fiche : docs/features/editeur-inline.md.
This commit is contained in:
2026-09-15 23:40:10 -04:00
parent f630771150
commit e3df4bbf00
16 changed files with 920 additions and 33 deletions
+115
View File
@@ -0,0 +1,115 @@
/**
* ObsiGate — Inline editor host (ROADMAP #93)
*
* The edition surface (`#editor-container`, containing the CodeMirror editor or
* the Forge iframe) used to live inside the full-screen `#editor-modal` overlay,
* which covered both the document read view and the AI assistant sidebar.
*
* This module moves that container **into the content area of the document
* being read** so editing replaces the read view instead of overlaying it:
*
* `#content-area` (or the active pane `.pane-content` in split view)
* └── .editor-container.editor-inline ← the editor, replacing the document
*
* Keeping the same DOM nodes (same ids: `#editor-body`, `#editor-save`, …) means
* CodeMirror, autosave, Yjs collaboration, the AI toolbar and the Forge iframe
* keep working untouched — only their host moves.
*
* Classes:
* `.editor-inline` — set on the container while it hosts an inline session
* `.editor-inline-host` — set on the content area it replaces
* `#editor-modal.editor-inline-mode` — the overlay is neutralised (transparent,
* zero-size, no scroll) but keeps `active`, so the mobile
* ribbon anchored to it (`.me-ribbon`) still works.
*/
export const INLINE_CONTAINER_CLASS = 'editor-inline';
export const INLINE_HOST_CLASS = 'editor-inline-host';
export const INLINE_MODAL_CLASS = 'editor-inline-mode';
/** The editor overlay element (`#editor-modal` when present). */
export function getEditorModal() {
return document.getElementById('editor-modal');
}
/**
* The editor container, wherever it currently lives (modal or content area).
* Falls back to a class lookup so the helper also works on a DOM where the id
* was not set (tests, older markup).
*/
export function getEditorContainer() {
return (
document.getElementById('editor-container') ||
document.querySelector('.editor-container.' + INLINE_CONTAINER_CLASS)
);
}
/**
* Content area that should host the inline editor: the pane currently being
* read. Mirrors viewer.js `getContentArea()` (multi-pane aware).
*/
export function getInlineHostArea() {
if (typeof window !== 'undefined') {
if (window._activePaneContentArea) return window._activePaneContentArea;
if (
window.PaneManager &&
typeof window.PaneManager.isSplit === 'function' &&
window.PaneManager.isSplit() &&
typeof window.getActiveContentArea === 'function'
) {
return window.getActiveContentArea();
}
}
return document.getElementById('content-area');
}
/** True when an inline edition session currently owns a content area. */
export function isInlineEditorActive() {
const container = getEditorContainer();
return !!(container && container.classList.contains(INLINE_CONTAINER_CLASS));
}
/**
* Move the editor container into `area`, replacing the rendered document.
* @returns {boolean} true when the container was mounted inline.
*/
export function mountEditorInline(area) {
const container = getEditorContainer();
const modal = getEditorModal();
if (!container || !modal || !area) return false;
// Already hosted by this very area — nothing to move.
if (container.parentElement === area) {
container.classList.add(INLINE_CONTAINER_CLASS);
area.classList.add(INLINE_HOST_CLASS);
modal.classList.add(INLINE_MODAL_CLASS);
return true;
}
area.classList.add(INLINE_HOST_CLASS);
area.replaceChildren(container);
container.classList.add(INLINE_CONTAINER_CLASS);
modal.classList.add(INLINE_MODAL_CLASS);
return true;
}
/**
* Move the editor container back into the modal and clear the inline classes.
* The host area is emptied (its read view is gone) and returned so the caller
* can re-render the document.
* @returns {HTMLElement|null} the content area that must be re-rendered.
*/
export function unmountEditorInline() {
const container = getEditorContainer();
const modal = getEditorModal();
if (!container || !modal) return null;
const parent = container.parentElement;
const host = parent && parent !== modal ? parent : null;
container.classList.remove(INLINE_CONTAINER_CLASS);
if (host) {
host.classList.remove(INLINE_HOST_CLASS);
if (typeof host.replaceChildren === 'function') host.replaceChildren();
else host.innerHTML = '';
}
modal.classList.remove(INLINE_MODAL_CLASS);
modal.appendChild(container);
return host;
}