diff --git a/public/index.html b/public/index.html
index d0efa8d..d443296 100644
--- a/public/index.html
+++ b/public/index.html
@@ -6,6 +6,8 @@
Tableau de bord — RdB
+
+
@@ -312,6 +357,7 @@
★ Vision
⏳ Frise
+ ✎ Rédaction
@@ -415,6 +461,26 @@
+
+
+
+
+
+
Choisir une scène dans le Binder
+
+
+
+
+ Enregistré
+
Enregistré
@@ -495,6 +561,10 @@ function switchView(name) {
if (name === 'vision') {
topTitle.textContent = 'Vision';
if (btnNew) btnNew.style.display = 'none';
+ } else if (name === 'redaction') {
+ topTitle.textContent = 'Rédaction';
+ if (btnNew) btnNew.style.display = 'none';
+ buildBinder();
} else {
topTitle.textContent = 'Frise';
if (btnNew) btnNew.style.display = '';
@@ -1287,6 +1357,237 @@ async function loadVision() {
}
loadVision();
+
+// ══════════════════════════════════════════════════════
+// ══ RÉDACTION — Binder + TipTap Editor ══
+// ══════════════════════════════════════════════════════
+
+let tiptapEditor = null;
+let currentSceneId = null;
+let saveTimeout = null;
+let wordCounts = {}; // sceneId → word count (cached)
+
+const ACTE_ORDER = ["Avant l'histoire", 'Acte 1', 'Acte 2', 'Acte 3'];
+
+function statusSlug(s) {
+ if (!s) return '';
+ return s.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
+}
+
+function buildBinder() {
+ const binder = document.getElementById('binder');
+ // Group scenes by acte (exclude "En attente", only include Scène and Événement types relevant to writing)
+ const grouped = {};
+ ACTE_ORDER.forEach(a => { grouped[a] = []; });
+
+ (records || []).forEach(r => {
+ const acte = r.Acte;
+ if (!acte || acte === 'En attente') return;
+ if (grouped[acte]) grouped[acte].push(r);
+ });
+
+ binder.innerHTML = '';
+ let totalWords = 0;
+
+ ACTE_ORDER.forEach(acte => {
+ const scenes = grouped[acte];
+ if (!scenes || scenes.length === 0) return;
+
+ const acteDiv = document.createElement('div');
+ acteDiv.className = 'binder-acte';
+
+ const title = document.createElement('div');
+ title.className = 'binder-acte-title';
+ title.textContent = acte;
+ acteDiv.appendChild(title);
+
+ scenes.sort((a, b) => (a.Ordre || 0) - (b.Ordre || 0));
+ scenes.forEach(scene => {
+ const el = document.createElement('div');
+ el.className = 'binder-scene' + (scene.Id === currentSceneId ? ' active' : '');
+ const slug = statusSlug(scene.Statut);
+ const wc = wordCounts[scene.Id] || 0;
+ totalWords += wc;
+ el.innerHTML = `${esc(scene.Title)}${wc ? `${wc}` : ''}`;
+ el.onclick = () => openScene(scene);
+ acteDiv.appendChild(el);
+ });
+
+ binder.appendChild(acteDiv);
+ });
+
+ // Total word count at bottom
+ const totalDiv = document.createElement('div');
+ totalDiv.style.cssText = 'padding: 16px 20px; margin-top: 12px; border-top: 1px solid var(--border); font-size: 12px; color: var(--text-hint);';
+ totalDiv.textContent = totalWords > 0 ? `Total : ${totalWords.toLocaleString('fr-FR')} mots` : '';
+ binder.appendChild(totalDiv);
+}
+
+async function openScene(scene) {
+ currentSceneId = scene.Id;
+ // Update binder selection
+ document.querySelectorAll('.binder-scene').forEach(el => el.classList.remove('active'));
+ event?.target?.closest?.('.binder-scene')?.classList.add('active');
+
+ // Show editor
+ document.getElementById('editor-empty').style.display = 'none';
+ const active = document.getElementById('editor-active');
+ active.style.display = 'flex';
+
+ // Header
+ document.getElementById('editor-title').textContent = scene.Title;
+ const meta = [];
+ if (scene.Acte) meta.push(scene.Acte);
+ if (scene.Type) meta.push(scene.Type);
+ if (scene.Statut) meta.push(scene.Statut);
+ if (scene.Personnage) meta.push(scene.Personnage);
+ document.getElementById('editor-meta').textContent = meta.join(' · ');
+
+ // Load content from API
+ try {
+ const res = await fetch(`/api/contenu/${scene.Id}`);
+ const row = await res.json();
+ const html = row?.Texte || '';
+ initEditor(html);
+ updateWordCount();
+ } catch (e) {
+ console.error('Load content error:', e);
+ initEditor('');
+ }
+}
+
+function initEditor(html) {
+ // Destroy previous editor
+ if (tiptapEditor) { tiptapEditor.destroy(); tiptapEditor = null; }
+
+ const { Editor } = window.TiptapCore || {};
+ const { StarterKit } = window.TiptapStarterKit || {};
+
+ if (!Editor || !StarterKit) {
+ // Fallback: contenteditable div if TipTap CDN not loaded
+ const el = document.getElementById('editor-content');
+ el.innerHTML = html || '';
+ el.contentEditable = 'true';
+ el.classList.add('tiptap');
+ el.addEventListener('input', () => { clearTimeout(saveTimeout); saveTimeout = setTimeout(saveContent, 1500); updateWordCount(); });
+ buildToolbarFallback();
+ return;
+ }
+
+ tiptapEditor = new Editor({
+ element: document.getElementById('editor-content'),
+ extensions: [StarterKit],
+ content: html || '',
+ onUpdate: () => {
+ clearTimeout(saveTimeout);
+ saveTimeout = setTimeout(saveContent, 1500);
+ updateWordCount();
+ },
+ });
+
+ buildToolbar();
+}
+
+function buildToolbar() {
+ const bar = document.getElementById('editor-toolbar');
+ if (!tiptapEditor) { buildToolbarFallback(); return; }
+
+ const btns = [
+ { label: 'G', cmd: () => tiptapEditor.chain().focus().toggleBold().run(), active: () => tiptapEditor.isActive('bold') },
+ { label: 'I', cmd: () => tiptapEditor.chain().focus().toggleItalic().run(), active: () => tiptapEditor.isActive('italic'), style: 'font-style:italic;' },
+ { label: 'H2', cmd: () => tiptapEditor.chain().focus().toggleHeading({ level: 2 }).run(), active: () => tiptapEditor.isActive('heading', { level: 2 }) },
+ { label: 'H3', cmd: () => tiptapEditor.chain().focus().toggleHeading({ level: 3 }).run(), active: () => tiptapEditor.isActive('heading', { level: 3 }) },
+ { label: '•', cmd: () => tiptapEditor.chain().focus().toggleBulletList().run(), active: () => tiptapEditor.isActive('bulletList') },
+ { label: '1.', cmd: () => tiptapEditor.chain().focus().toggleOrderedList().run(), active: () => tiptapEditor.isActive('orderedList') },
+ { label: '❝', cmd: () => tiptapEditor.chain().focus().toggleBlockquote().run(), active: () => tiptapEditor.isActive('blockquote') },
+ { label: '—', cmd: () => tiptapEditor.chain().focus().setHorizontalRule().run(), active: () => false },
+ ];
+
+ bar.innerHTML = '';
+ btns.forEach(b => {
+ const btn = document.createElement('button');
+ btn.innerHTML = b.label;
+ if (b.style) btn.style.cssText = b.style;
+ btn.onclick = b.cmd;
+ bar.appendChild(btn);
+ });
+
+ // Update active states on selection change
+ tiptapEditor.on('selectionUpdate', () => {
+ bar.querySelectorAll('button').forEach((btn, i) => {
+ btn.classList.toggle('is-active', btns[i].active());
+ });
+ });
+ tiptapEditor.on('update', () => {
+ bar.querySelectorAll('button').forEach((btn, i) => {
+ btn.classList.toggle('is-active', btns[i].active());
+ });
+ });
+}
+
+function buildToolbarFallback() {
+ const bar = document.getElementById('editor-toolbar');
+ bar.innerHTML = 'Éditeur simplifié — TipTap non chargé';
+}
+
+function updateWordCount() {
+ let text = '';
+ if (tiptapEditor) {
+ text = tiptapEditor.getText();
+ } else {
+ const el = document.getElementById('editor-content');
+ text = el?.textContent || '';
+ }
+ const count = text.trim() ? text.trim().split(/\s+/).length : 0;
+ document.getElementById('word-count').textContent = `${count.toLocaleString('fr-FR')} mots`;
+ if (currentSceneId) wordCounts[currentSceneId] = count;
+}
+
+async function saveContent() {
+ if (!currentSceneId) return;
+ let html = '';
+ let wordCount = 0;
+ if (tiptapEditor) {
+ html = tiptapEditor.getHTML();
+ const text = tiptapEditor.getText();
+ wordCount = text.trim() ? text.trim().split(/\s+/).length : 0;
+ } else {
+ const el = document.getElementById('editor-content');
+ html = el?.innerHTML || '';
+ const text = el?.textContent || '';
+ wordCount = text.trim() ? text.trim().split(/\s+/).length : 0;
+ }
+
+ try {
+ await fetch(`/api/contenu/${currentSceneId}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ Texte: html, Mots: wordCount }),
+ });
+ wordCounts[currentSceneId] = wordCount;
+ // Show saved indicator
+ const el = document.getElementById('editor-saving');
+ el.classList.add('show');
+ setTimeout(() => el.classList.remove('show'), 1500);
+ // Update binder word count
+ buildBinder();
+ } catch (e) {
+ console.error('Save error:', e);
+ }
+}
+
+// Load word counts on init (for binder display)
+async function loadWordCounts() {
+ try {
+ const res = await fetch('/api/contenu');
+ if (res.status === 401) return;
+ const data = await res.json();
+ (data.list || []).forEach(r => {
+ if (r.SceneId && r.Mots) wordCounts[r.SceneId] = r.Mots;
+ });
+ } catch (e) { /* silent */ }
+}
+loadWordCounts();