diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/public/app.js b/public/app.js new file mode 100644 index 0000000..732d1b6 --- /dev/null +++ b/public/app.js @@ -0,0 +1,2408 @@ +const ACTES_FRISE = ["Prologue", "Acte 1", "Acte 2", "Acte 3", "Épilogue"]; +let records = []; +let columns = {}; +let editingId = null; + +// ── View switching ── +function switchView(name) { + document.querySelectorAll('.view').forEach(v => v.classList.remove('active')); + document.getElementById('view-' + name).classList.add('active'); + document.querySelectorAll('.nav-item[data-view]').forEach(n => n.classList.toggle('active', n.dataset.view === name)); + try { localStorage.setItem('tdb-rdb-view', name); } catch(e) {} + const topTitle = document.querySelector('.topbar-title'); + const btnNew = document.querySelector('.btn-new'); + // Hide topbar new button (each page has its own) + const topBtnNew = document.querySelector('.topbar-right > .btn-new'); + if (topBtnNew) topBtnNew.style.display = 'none'; + + if (name === 'vision') { + topTitle.textContent = 'Vision'; + } else if (name === 'redaction') { + topTitle.textContent = 'Rédaction'; + buildBinder(); + } else if (name === 'personnages') { + topTitle.textContent = 'Personnages'; + loadPersonnages(); + } else if (name === 'ressources') { + topTitle.textContent = 'Ressources'; + loadRessources(); + } else { + topTitle.textContent = 'Frise'; + if (topBtnNew) topBtnNew.style.display = ''; + } +} + +// ── Dark mode ── +function toggleDark() { + document.body.classList.toggle('dark'); + localStorage.setItem('tdb-dark', document.body.classList.contains('dark') ? '1' : '0'); +} +if (localStorage.getItem('tdb-dark') === '1') document.body.classList.add('dark'); + +// ── Filters ── +function populateFilterOptions() { + // Collect unique personnages from data + const persoSet = new Set(); + records.forEach(r => { + if (r.Personnage) r.Personnage.split(',').forEach(p => persoSet.add(p.trim())); + }); + const persoSel = document.getElementById('filter-perso'); + const curPerso = persoSel.value; + persoSel.innerHTML = ''; + [...persoSet].sort().forEach(p => { + const opt = document.createElement('option'); + opt.value = p; opt.textContent = p; + persoSel.appendChild(opt); + }); + persoSel.value = curPerso; + + // Tags from data (free text, not from column options) + const tagSet = new Set(); + records.forEach(r => { + if (r.Tags) r.Tags.split(',').forEach(t => { const trimmed = t.trim(); if (trimmed) tagSet.add(trimmed); }); + }); + const tagSel = document.getElementById('filter-tag'); + const curTag = tagSel.value; + tagSel.innerHTML = ''; + [...tagSet].sort((a, b) => a.localeCompare(b, 'fr')).forEach(t => { + const opt = document.createElement('option'); + opt.value = t; opt.textContent = t; + tagSel.appendChild(opt); + }); + tagSel.value = curTag; + + // Populate Rédaction filters (same data) + const redPersoSel = document.getElementById('red-filter-perso'); + if (redPersoSel) { + const curRP = redPersoSel.value; + while (redPersoSel.options.length > 1) redPersoSel.remove(1); + [...persoSet].sort().forEach(p => { + const opt = document.createElement('option'); + opt.value = p; opt.textContent = p; + redPersoSel.appendChild(opt); + }); + redPersoSel.value = curRP; + } + const redTagSel = document.getElementById('red-filter-tag'); + if (redTagSel) { + const curRT = redTagSel.value; + while (redTagSel.options.length > 1) redTagSel.remove(1); + [...tagSet].sort((a, b) => a.localeCompare(b, 'fr')).forEach(t => { + const opt = document.createElement('option'); + opt.value = t; opt.textContent = t; + redTagSel.appendChild(opt); + }); + redTagSel.value = curRT; + } +} + +function applyFilters() { + // Re-render frise if sort changed (sort is applied during render) + renderFrise(); + + const perso = document.getElementById('filter-perso').value; + const tag = document.getElementById('filter-tag').value; + const search = document.getElementById('filter-search').value.toLowerCase().trim(); + + document.querySelectorAll('.card').forEach(card => { + const id = Number(card.dataset.id); + const r = records.find(rec => rec.Id === id); + if (!r) return; + + let visible = true; + if (perso && !(r.Personnage || '').split(',').map(s => s.trim()).includes(perso)) visible = false; + if (tag && !(r.Tags || '').split(',').map(s => s.trim()).includes(tag)) visible = false; + if (search) { + const haystack = [r.Title, r['Résumé'], r.Personnage, r['Intérêt narratif'], r.Implications, r.Origine].filter(Boolean).join(' ').toLowerCase(); + if (!haystack.includes(search)) visible = false; + } + + card.classList.toggle('filtered-out', !visible); + }); + + // Resize acte sections based on visible cards + document.querySelectorAll('.acte-section').forEach(section => { + const visibleCards = section.querySelectorAll('.card:not(.filtered-out)').length; + const minW = Math.max(280, visibleCards * 274 + 20); + section.style.width = minW + 'px'; + }); +} + +function clearFilters() { + document.getElementById('filter-perso').value = ''; + document.getElementById('filter-tag').value = ''; + document.getElementById('filter-search').value = ''; + document.getElementById('filter-sort').value = 'ordre'; + applyFilters(); +} + +const STATUS_TAGS = { 'À étudier': 'badge-etudier', 'Validée': 'badge-validee', 'Abandonnée': 'badge-abandonnee', 'En conflit': 'badge-conflit' }; +const STATUS_COLORS = { 'À étudier': '#c4a030', 'Validée': '#5a7a3a', 'Abandonnée': '#a08a76', 'En conflit': '#8b3030' }; +function tagBadgeClass(tagName) { + return STATUS_TAGS[tagName] || 'badge-tag'; +} + +function makeCard(r) { + const card = document.createElement('div'); + card.className = 'card'; + card.dataset.id = r.Id; + card.dataset.acte = r.Acte || 'En attente'; + card.onclick = () => openEdit(r.Id); + + const persos = r.Personnage ? r.Personnage.split(',').map(p => + `${esc(p.trim())}` + ).join('') : ''; + + const likeE = r.LikeE ? 'liked' : ''; + const likeM = r.LikeM ? 'liked' : ''; + const comments = parseComments(r.Commentaires); + const commentBadge = comments.length > 0 ? `${comments.length}` : ''; + + card.innerHTML = ` +
${esc(r.Title || '')}
+ ${r['Résumé'] ? `
${renderMarkdown(r['Résumé'])}
` : ''} +
+ ${(r.Tags || '').split(',').filter(t => t.trim()).map(t => `${esc(t.trim())}`).join('')} + ${persos} + ${commentBadge} + + + + +
+ `; + return card; +} + +async function toggleLike(e, id, who) { + e.stopPropagation(); + const r = records.find(r => r.Id === id); + if (!r) return; + const field = who === 'E' ? 'LikeE' : 'LikeM'; + const newVal = r[field] ? 0 : 1; + r[field] = newVal; + renderFrise(); + await fetch(`/api/records/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ [field]: newVal }), + }); +} + +// ── Card comments ── +let commentAuthor = 'E'; + +function parseComments(raw) { + if (!raw) return []; + try { return JSON.parse(raw); } catch { return []; } +} + +function renderCardComments(comments) { + const list = document.getElementById('card-comment-list'); + list.innerHTML = comments.map((c, i) => ` +
+
+ ${c.auteur === 'M' ? 'Myriam' : 'Etienne'} + ${esc(c.date || '')} + +
+
${esc(c.texte || '')}
+
+ `).join(''); +} + +async function addCardComment() { + if (!editingId) return; + const input = document.getElementById('card-comment-input'); + const texte = input.value.trim(); + if (!texte) return; + + const r = records.find(r => r.Id === editingId); + if (!r) return; + + const comments = parseComments(r.Commentaires); + const now = new Date().toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' }); + comments.push({ auteur: commentAuthor, date: now, texte }); + + const json = JSON.stringify(comments); + r.Commentaires = json; + renderCardComments(comments); + input.value = ''; + + await fetch(`/api/records/${editingId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ Commentaires: json }), + }); + renderFrise(); +} + +async function deleteCardComment(index) { + if (!editingId) return; + const r = records.find(r => r.Id === editingId); + if (!r) return; + + const comments = parseComments(r.Commentaires); + comments.splice(index, 1); + + const json = comments.length > 0 ? JSON.stringify(comments) : null; + r.Commentaires = json; + renderCardComments(comments); + + await fetch(`/api/records/${editingId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ Commentaires: json || '' }), + }); + renderFrise(); +} + +function toggleCommentAuthor() { + commentAuthor = commentAuthor === 'E' ? 'M' : 'E'; + const btn = document.getElementById('comment-author-btn'); + btn.textContent = commentAuthor === 'E' ? 'Etienne' : 'Myriam'; +} + +function sortRecords(a, b, mode) { + if (mode === 'date-desc') return new Date(b.CreatedAt || 0) - new Date(a.CreatedAt || 0); + if (mode === 'date-asc') return new Date(a.CreatedAt || 0) - new Date(b.CreatedAt || 0); + if (mode === 'alpha') return (a.Title || '').localeCompare(b.Title || '', 'fr'); + return (a.Ordre || 0) - (b.Ordre || 0); +} + +function renderFrise() { + const area = document.getElementById('timeline-area'); + area.innerHTML = ''; + + // Build horizontal timeline + const container = document.createElement('div'); + container.style.cssText = 'display:flex; min-width:min-content;'; + + const sortMode = document.getElementById('filter-sort')?.value || 'ordre'; + + ACTES_FRISE.forEach(acte => { + const acteRecords = records + .filter(r => (r.Acte || 'En attente') === acte) + .sort((a, b) => sortRecords(a, b, sortMode)); + + const section = document.createElement('div'); + section.className = 'acte-section'; + section.dataset.acte = acte; + + section.innerHTML = ` +
${esc(acte)}
+
+ `; + + const row = document.createElement('div'); + row.className = 'cards-row'; + row.dataset.acte = acte; + + acteRecords.forEach(r => row.appendChild(makeCard(r))); + + section.appendChild(row); + container.appendChild(section); + + // SortableJS on horizontal row + new Sortable(row, { + group: 'frise', + animation: 200, + direction: 'horizontal', + ghostClass: 'sortable-ghost', + chosenClass: 'sortable-chosen', + onEnd: handleDrop, + }); + }); + + area.appendChild(container); + + // Waiting zone + const waitingCards = document.getElementById('waiting-cards'); + waitingCards.innerHTML = ''; + const waitingRecords = records + .filter(r => !r.Acte || r.Acte === 'En attente') + .sort((a, b) => sortRecords(a, b, sortMode)); + + waitingRecords.forEach(r => waitingCards.appendChild(makeCard(r))); + + new Sortable(waitingCards, { + group: 'frise', + animation: 200, + direction: 'horizontal', + ghostClass: 'sortable-ghost', + chosenClass: 'sortable-chosen', + onEnd: handleDrop, + }); +} + +async function handleDrop(evt) { + const toContainer = evt.to; + const newActe = toContainer.dataset.acte || 'En attente'; + + // Collect new order for target + const updates = []; + const cards = toContainer.querySelectorAll('.card'); + cards.forEach((c, i) => { + updates.push({ id: Number(c.dataset.id), Acte: newActe, Ordre: i + 1 }); + }); + + // Reorder source if different + if (evt.from !== evt.to) { + const srcActe = evt.from.dataset.acte || 'En attente'; + const srcCards = evt.from.querySelectorAll('.card'); + srcCards.forEach((c, i) => { + updates.push({ id: Number(c.dataset.id), Acte: srcActe, Ordre: i + 1 }); + }); + } + + // Update local state + updates.forEach(u => { + const rec = records.find(r => r.Id === u.id); + if (rec) { rec.Acte = u.Acte; rec.Ordre = u.Ordre; } + }); + + await fetch('/api/reorder', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(updates), + }); +} + +function populateSelects() { + ['Acte'].forEach(name => { + const sel = document.getElementById(`f-${name.toLowerCase()}`); + if (!sel || !columns[name]) return; + while (sel.options.length > 1) sel.remove(1); + columns[name].options.forEach(o => { + const opt = document.createElement('option'); + opt.value = o; opt.textContent = o; + sel.appendChild(opt); + }); + }); + + const container = document.getElementById('f-perso'); + container.innerHTML = ''; + if (columns.Personnage) { + columns.Personnage.options.forEach(p => { + const tag = document.createElement('span'); + tag.className = 'ms-tag'; + tag.textContent = p; + tag.onclick = () => tag.classList.toggle('selected'); + container.appendChild(tag); + }); + } + const addBtn = document.createElement('span'); + addBtn.className = 'ms-add'; + addBtn.textContent = '+'; + addBtn.title = 'Ajouter un personnage'; + addBtn.onclick = async () => { + const name = prompt('Nom du personnage :'); + if (!name || !name.trim()) return; + const trimmed = name.trim(); + if (columns.Personnage && columns.Personnage.options.includes(trimmed)) { + alert('Ce personnage existe déjà.'); + return; + } + const res = await fetch('/api/columns/personnage', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: trimmed }), + }); + if (res.ok) { + if (!columns.Personnage) columns.Personnage = { type: 'MultiSelect', options: [] }; + columns.Personnage.options.push(trimmed); + const tag = document.createElement('span'); + tag.className = 'ms-tag selected'; + tag.textContent = trimmed; + tag.onclick = () => tag.classList.toggle('selected'); + container.insertBefore(tag, addBtn); + } else { + alert('Erreur lors de l\'ajout.'); + } + }; + container.appendChild(addBtn); +} + +function openNew() { + editingId = null; + document.getElementById('modal-label').textContent = 'Nouvelle idee'; + document.getElementById('f-title').value = ''; + renderModalTags(['À étudier']); + document.getElementById('f-tags-input').value = ''; + document.getElementById('f-acte').value = 'En attente'; + document.getElementById('f-resume').value = ''; + document.getElementById('f-interet').value = ''; + document.getElementById('f-implications').value = ''; + document.getElementById('f-origine').value = ''; + document.getElementById('f-lien').value = ''; + document.querySelectorAll('#f-perso .ms-tag').forEach(t => t.classList.remove('selected')); + document.getElementById('btn-delete').style.display = 'none'; + document.getElementById('btn-history-frise').style.display = 'none'; + document.getElementById('btn-rediger').style.display = 'none'; + document.getElementById('card-comments-section').style.display = 'none'; + document.getElementById('card-comment-list').innerHTML = ''; + document.getElementById('card-comment-input').value = ''; + document.getElementById('modal').classList.add('open'); +} + +function openEdit(id) { + const r = records.find(rec => rec.Id === id); + if (!r) return; + editingId = id; + document.getElementById('modal-label').textContent = 'Modifier'; + document.getElementById('f-title').value = r.Title || ''; + renderModalTags((r.Tags || '').split(',').map(s => s.trim()).filter(Boolean)); + document.getElementById('f-tags-input').value = ''; + document.getElementById('f-acte').value = r.Acte || ''; + document.getElementById('f-resume').value = r['Résumé'] || ''; + document.getElementById('f-interet').value = r['Intérêt narratif'] || ''; + document.getElementById('f-implications').value = r.Implications || ''; + document.getElementById('f-origine').value = r.Origine || ''; + document.getElementById('f-lien').value = r['Lien rédaction'] || ''; + + const selected = r.Personnage ? r.Personnage.split(',').map(s => s.trim()) : []; + document.querySelectorAll('#f-perso .ms-tag').forEach(t => { + t.classList.toggle('selected', selected.includes(t.textContent)); + }); + + document.getElementById('btn-delete').style.display = 'block'; + document.getElementById('btn-history-frise').style.display = ''; + document.getElementById('btn-rediger').style.display = ''; + + // Comments + const comments = parseComments(r.Commentaires); + document.getElementById('card-comments-section').style.display = 'block'; + renderCardComments(comments); + document.getElementById('card-comment-input').value = ''; + + document.getElementById('modal').classList.add('open'); +} + +function closeModal() { + document.getElementById('modal').classList.remove('open'); + editingId = null; +} + +async function saveCard() { + const persos = []; + document.querySelectorAll('#f-perso .ms-tag.selected').forEach(t => persos.push(t.textContent)); + + const data = { + Title: document.getElementById('f-title').value, + Tags: getModalTags().join(', ') || null, + Acte: document.getElementById('f-acte').value || 'En attente', + Personnage: persos.join(',') || null, + 'Résumé': document.getElementById('f-resume').value || null, + 'Intérêt narratif': document.getElementById('f-interet').value || null, + Implications: document.getElementById('f-implications').value || null, + Origine: document.getElementById('f-origine').value || null, + 'Lien rédaction': document.getElementById('f-lien').value || null, + }; + + if (editingId) { + await fetch(`/api/records/${editingId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }); + } else { + data.Ordre = records.filter(r => r.Acte === data.Acte).length + 1; + await fetch('/api/records', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }); + } + + closeModal(); + await loadData(); +} + +async function deleteCard() { + if (!editingId) return; + if (!confirm('Supprimer cette idee ?')) return; + await fetch(`/api/records/${editingId}`, { method: 'DELETE' }); + closeModal(); + await loadData(); +} + +function goToRedaction(id) { + const scene = records.find(r => r.Id === id); + if (!scene) return; + closeModal(); + switchView('redaction'); + setTimeout(() => openScene(scene), 300); +} + +async function loadData() { + const [recRes, colRes] = await Promise.all([ + fetch('/api/records'), + fetch('/api/columns'), + ]); + if (recRes.status === 401) { window.location.reload(); return; } + const recData = await recRes.json(); + records = recData.list || []; + columns = await colRes.json(); + populateSelects(); + populateFilterOptions(); + applyFilters(); + // Restore last active view + try { + const saved = localStorage.getItem('tdb-rdb-view'); + if (saved && document.getElementById('view-' + saved)) switchView(saved); + } catch(e) {} +} + +function esc(s) { + const d = document.createElement('div'); + d.textContent = s; + return d.innerHTML; +} + +// Sanitize HTML string and return a DocumentFragment (safe for appendChild) +function createSanitizedFragment(html) { + const clean = typeof DOMPurify !== 'undefined' ? DOMPurify.sanitize(html) : esc(html); + const tpl = document.createElement('template'); + tpl.innerHTML = clean; + return tpl.content; +} + +// Render Markdown to sanitized HTML string +function renderMarkdown(text) { + if (!text) return ''; + const raw = typeof marked !== 'undefined' ? marked.parse(text) : esc(text); + return typeof DOMPurify !== 'undefined' ? DOMPurify.sanitize(raw) : esc(text); +} + +// ── Tags input ── +function getModalTags() { + return [...document.querySelectorAll('#f-tags .tag-chip')].map(c => c.dataset.tag); +} + +function renderModalTags(tags) { + const container = document.getElementById('f-tags'); + container.querySelectorAll('.tag-chip').forEach(c => c.remove()); + const input = document.getElementById('f-tags-input'); + (tags || []).forEach(tag => { + const chip = document.createElement('span'); + chip.className = 'tag-chip'; + chip.dataset.tag = tag; + chip.innerHTML = `${esc(tag)}×`; + container.insertBefore(chip, input); + }); +} + +function addTag() { + const input = document.getElementById('f-tags-input'); + const val = input.value.trim(); + if (!val) return; + const current = getModalTags(); + if (!current.includes(val)) { + renderModalTags([...current, val]); + } + input.value = ''; +} + +document.getElementById('modal').addEventListener('click', e => { + if (e.target === document.getElementById('modal')) closeModal(); +}); +document.addEventListener('keydown', e => { + if (e.key === 'Escape') { + closeModal(); + if (typeof closePersoModal === 'function') closePersoModal(); + if (typeof closeResModal === 'function') closeResModal(); + } +}); + +loadData(); + +// ══════════════════════════════════════ +// ══ VISION — Editable sections ══ +// ══════════════════════════════════════ + +let visionData = {}; // section -> {Id, Contenu} + +const PLACEHOLDERS = { + 'trois-mots': 'Les trois mots qui définissent votre univers...', + 'hero-sous-titre': 'Sous-titre ou accroche...', + 'phrase': 'Cliquez pour écrire la phrase qui résume votre histoire...', + 'phrase-detail': 'Un détail, une nuance, une précision...', + 'synopsis': 'Développez ici le résumé de votre histoire en quelques paragraphes...', +}; + +const FIELD_MAP = { + 'trois-mots': 'v-trois-mots', + 'hero-sous-titre': 'v-hero-sub', + 'phrase': 'v-phrase', + 'phrase-detail': 'v-phrase-detail', + 'synopsis': 'v-synopsis', +}; + +function showSaved() { + const el = document.getElementById('vision-saving'); + el.classList.add('show'); + setTimeout(() => el.classList.remove('show'), 1200); +} + +async function saveVisionSection(section, contenu) { + const row = visionData[section]; + if (!row) return; + row.Contenu = contenu; // update local cache immediately + fetch(`/api/vision/${row.Id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ Contenu: contenu }), + }).then(() => showSaved()); +} + +function setupEditable(elId, section) { + const el = document.getElementById(elId); + if (!el) return; + const placeholder = PLACEHOLDERS[section] || ''; + + function updatePlaceholder() { + const text = el.textContent.trim(); + if (!text || text === placeholder) { + el.classList.add('placeholder'); + if (!text) el.textContent = placeholder; + } else { + el.classList.remove('placeholder'); + } + } + + el.addEventListener('focus', () => { + if (el.classList.contains('placeholder')) { + el.textContent = ''; + el.classList.remove('placeholder'); + } + }); + + el.addEventListener('blur', () => { + const text = el.textContent.trim(); + if (text && text !== placeholder) { + const row = visionData[section]; + if (row && row.Contenu !== text) { + saveVisionSection(section, text); + } + } else { + el.textContent = placeholder; + el.classList.add('placeholder'); + const row = visionData[section]; + if (row && row.Contenu) { + saveVisionSection(section, ''); + } + } + }); + + // Prevent Enter creating divs in single-line fields + if (section !== 'synopsis') { + el.addEventListener('keydown', e => { + if (e.key === 'Enter') { e.preventDefault(); el.blur(); } + }); + } + + updatePlaceholder(); +} + +// ── Tags ── +function renderTags() { + const container = document.getElementById('v-tags'); + const row = visionData['tags']; + let tags = []; + try { tags = JSON.parse(row?.Contenu || '[]'); } catch { tags = []; } + // Normalize: old format {label, type} → just strings + tags = tags.map(t => typeof t === 'string' ? t : t.label || ''); + + container.innerHTML = ''; + tags.forEach((label, i) => { + const span = document.createElement('span'); + span.className = 'tag ambiance'; + span.innerHTML = `${esc(label)}×`; + container.appendChild(span); + }); + + const addBtn = document.createElement('button'); + addBtn.className = 'tag-add'; + addBtn.textContent = '+ Ajouter'; + addBtn.onclick = () => { + const label = prompt('Nouveau tag :'); + if (!label) return; + tags.push(label); + saveVisionSection('tags', JSON.stringify(tags)); + renderTags(); + }; + container.appendChild(addBtn); +} + +function removeTag(index) { + const row = visionData['tags']; + let tags = []; + try { tags = JSON.parse(row?.Contenu || '[]'); } catch { return; } + tags = tags.map(t => typeof t === 'string' ? t : t.label || ''); + tags.splice(index, 1); + saveVisionSection('tags', JSON.stringify(tags)); + renderTags(); +} + +// ── Lists (on-veut / on-ne-veut-pas) — items with title + optional description ── +function renderList(section, containerId, cssClass) { + const container = document.getElementById(containerId); + const row = visionData[section]; + let items = []; + try { items = JSON.parse(row?.Contenu || '[]'); } catch { items = []; } + // Normalize: old format was plain strings, new format is {title, desc} + items = items.map(it => typeof it === 'string' ? { title: it, desc: '' } : it); + + container.innerHTML = ''; + items.forEach((item, i) => { + const hasDesc = !!item.desc; + const div = document.createElement('div'); + div.className = `list-item ${cssClass}${hasDesc ? ' expanded' : ''}`; + + const content = document.createElement('div'); + content.className = 'list-item-text'; + content.style.flex = '1'; + + const titleEl = document.createElement('div'); + titleEl.className = 'list-item-title'; + titleEl.contentEditable = 'true'; + titleEl.textContent = item.title; + titleEl.addEventListener('blur', () => { + const v = titleEl.textContent.trim(); + if (v && v !== items[i].title) { items[i].title = v; saveVisionSection(section, JSON.stringify(items)); } + }); + titleEl.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); titleEl.blur(); } }); + content.appendChild(titleEl); + + const descEl = document.createElement('div'); + descEl.className = 'list-item-desc'; + descEl.contentEditable = 'true'; + descEl.setAttribute('data-placeholder', 'Développer... (Markdown : - item pour les listes)'); + descEl.style.display = 'none'; + // Render Markdown preview (sanitized) + const descPreview = document.createElement('div'); + descPreview.className = 'list-item-desc-preview'; + descPreview.style.display = 'none'; + if (hasDesc) { + const rawHtml = typeof marked !== 'undefined' ? marked.parse(item.desc) : esc(item.desc); + descPreview.textContent = ''; + descPreview.appendChild(createSanitizedFragment(rawHtml)); + descPreview.style.display = 'block'; + } + descPreview.onclick = () => { + descPreview.style.display = 'none'; + descEl.style.display = 'block'; + descEl.textContent = items[i].desc || ''; + descEl.focus(); + }; + descEl.addEventListener('blur', () => { + const v = descEl.innerText.trim(); + if (v !== (items[i].desc || '')) { items[i].desc = v; saveVisionSection(section, JSON.stringify(items)); } + descEl.style.display = 'none'; + if (v) { + const rawHtml = typeof marked !== 'undefined' ? marked.parse(v) : esc(v); + descPreview.textContent = ''; + descPreview.appendChild(createSanitizedFragment(rawHtml)); + descPreview.style.display = 'block'; + } else { + descPreview.style.display = 'none'; + div.classList.remove('expanded'); + expandBtn.textContent = '▸'; + expandBtn.title = 'Développer'; + } + }); + content.appendChild(descPreview); + content.appendChild(descEl); + + const bullet = document.createElement('span'); + bullet.className = 'bullet'; + + const actions = document.createElement('span'); + actions.style.cssText = 'display:flex; gap:6px; align-items:center;'; + + const expandBtn = document.createElement('span'); + expandBtn.className = 'list-item-expand'; + expandBtn.textContent = hasDesc ? '▾' : '▸'; + expandBtn.title = hasDesc ? 'Réduire' : 'Développer'; + expandBtn.style.cursor = 'pointer'; + expandBtn.onclick = () => { + if (div.classList.contains('expanded')) { + div.classList.remove('expanded'); + descPreview.style.display = 'none'; + descEl.style.display = 'none'; + expandBtn.textContent = '▸'; + expandBtn.title = 'Développer'; + } else { + div.classList.add('expanded'); + if (items[i].desc) { + const rawHtml = typeof marked !== 'undefined' ? marked.parse(items[i].desc) : esc(items[i].desc); + descPreview.textContent = ''; + descPreview.appendChild(createSanitizedFragment(rawHtml)); + descPreview.style.display = 'block'; + } else { + descEl.style.display = 'block'; + descEl.textContent = ''; + descEl.focus(); + } + expandBtn.textContent = '▾'; + expandBtn.title = 'Réduire'; + } + }; + if (hasDesc) div.classList.add('expanded'); + + const removeBtn = document.createElement('span'); + removeBtn.className = 'list-item-remove'; + removeBtn.innerHTML = '×'; + removeBtn.onclick = () => { items.splice(i, 1); saveVisionSection(section, JSON.stringify(items)); renderList(section, containerId, cssClass); }; + + actions.appendChild(expandBtn); + actions.appendChild(removeBtn); + + div.appendChild(bullet); + div.appendChild(content); + div.appendChild(actions); + container.appendChild(div); + }); + + const addEl = document.createElement('div'); + addEl.className = 'list-add'; + addEl.innerHTML = '+ Ajouter un élément'; + addEl.onclick = () => { + const text = prompt('Nouvel élément :'); + if (!text) return; + items.push({ title: text, desc: '' }); + saveVisionSection(section, JSON.stringify(items)); + renderList(section, containerId, cssClass); + }; + container.appendChild(addEl); +} + +function removeListItem(section, containerId, cssClass, index) { + const row = visionData[section]; + let items = []; + try { items = JSON.parse(row?.Contenu || '[]'); } catch { return; } + items.splice(index, 1); + saveVisionSection(section, JSON.stringify(items)); + renderList(section, containerId, cssClass); +} + +// ── Moodboard (images + free text captions) ── +function renderMoodboard() { + const container = document.getElementById('v-moodboard'); + const row = visionData['moodboard']; + let items = []; + try { items = JSON.parse(row?.Contenu || '[]'); } catch { items = []; } + + container.innerHTML = ''; + items.forEach((item, i) => { + const card = document.createElement('div'); + card.className = 'mood-card'; + + const imgDiv = document.createElement('div'); + const hasImg = !!item.image; + imgDiv.className = 'mood-card-img' + (hasImg ? ' has-image' : ' empty'); + if (hasImg) { + const zoom = item.zoom ?? 100; + imgDiv.style.backgroundImage = `url(${item.image})`; + imgDiv.style.backgroundSize = `${zoom}%`; + imgDiv.style.backgroundPosition = `${item.posX ?? 50}% ${item.posY ?? 50}%`; + } + if (!hasImg) { + imgDiv.onclick = () => { moodUploadTarget = i; document.getElementById('mood-file-input').click(); }; + } else { + // Drag to reposition (2D) — double-click to replace image + let startX, startY, startPosX, startPosY, didDrag; + const onMove = (e) => { + const dx = e.clientX - startX, dy = e.clientY - startY; + if (!didDrag && Math.abs(dx) < 4 && Math.abs(dy) < 4) return; + didDrag = true; + const rect = imgDiv.getBoundingClientRect(); + const scale = (item.zoom ?? 100) / 100; + item.posX = Math.max(0, Math.min(100, startPosX + (dx / rect.width) * -80 / scale)); + item.posY = Math.max(0, Math.min(100, startPosY + (dy / rect.height) * -80 / scale)); + imgDiv.style.backgroundPosition = `${item.posX}% ${item.posY}%`; + }; + const onUp = () => { + imgDiv.classList.remove('dragging'); + window.removeEventListener('mousemove', onMove); + window.removeEventListener('mouseup', onUp); + if (didDrag) saveVisionSection('moodboard', JSON.stringify(items)); + }; + imgDiv.addEventListener('mousedown', (e) => { + if (e.button !== 0) return; + e.preventDefault(); + didDrag = false; + imgDiv.classList.add('dragging'); + startX = e.clientX; startY = e.clientY; + startPosX = item.posX ?? 50; startPosY = item.posY ?? 50; + window.addEventListener('mousemove', onMove); + window.addEventListener('mouseup', onUp); + }); + // Ctrl+scroll to zoom (prevents accidental zoom) + imgDiv.addEventListener('wheel', (e) => { + if (!e.ctrlKey && !e.metaKey) return; + e.preventDefault(); + const delta = e.deltaY > 0 ? -10 : 10; + item.zoom = Math.max(100, Math.min(400, (item.zoom ?? 100) + delta)); + imgDiv.style.backgroundSize = `${item.zoom}%`; + saveVisionSection('moodboard', JSON.stringify(items)); + }, { passive: false }); + imgDiv.addEventListener('dblclick', () => { moodUploadTarget = i; document.getElementById('mood-file-input').click(); }); + } + + const caption = document.createElement('div'); + caption.className = 'mood-card-caption'; + caption.contentEditable = 'true'; + caption.textContent = item.caption || ''; + caption.setAttribute('placeholder', 'Légende...'); + caption.addEventListener('blur', () => { + const v = caption.textContent.trim(); + if (v !== (items[i].caption || '')) { + items[i].caption = v; + saveVisionSection('moodboard', JSON.stringify(items)); + } + }); + + const removeBtn = document.createElement('button'); + removeBtn.className = 'mood-card-remove'; + removeBtn.innerHTML = '×'; + removeBtn.onclick = (e) => { e.stopPropagation(); removeMoodItem(i); }; + + // Drag handle for reordering + const handle = document.createElement('div'); + handle.className = 'mood-card-handle'; + handle.innerHTML = '⠿'; + if (hasImg) { + card.draggable = false; + handle.addEventListener('mousedown', () => { card.draggable = true; }); + window.addEventListener('mouseup', () => { card.draggable = false; }); + card.addEventListener('dragstart', (e) => { + moodDragIndex = i; + card.classList.add('dragging-card'); + e.dataTransfer.effectAllowed = 'move'; + }); + card.addEventListener('dragend', () => { + card.classList.remove('dragging-card'); + card.draggable = false; + moodDragIndex = -1; + container.querySelectorAll('.mood-card').forEach(c => c.classList.remove('drag-over')); + }); + card.addEventListener('dragover', (e) => { + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + card.classList.add('drag-over'); + }); + card.addEventListener('dragleave', () => { card.classList.remove('drag-over'); }); + card.addEventListener('drop', (e) => { + e.preventDefault(); + card.classList.remove('drag-over'); + if (moodDragIndex < 0 || moodDragIndex === i) return; + const moved = items.splice(moodDragIndex, 1)[0]; + items.splice(i, 0, moved); + saveVisionSection('moodboard', JSON.stringify(items)); + renderMoodboard(); + }); + } + + card.appendChild(removeBtn); + card.appendChild(handle); + card.appendChild(imgDiv); + card.appendChild(caption); + container.appendChild(card); + }); + + const addCard = document.createElement('div'); + addCard.className = 'mood-add'; + addCard.innerHTML = '
+
'; + addCard.onclick = () => { + items.push({ caption: '', image: '' }); + saveVisionSection('moodboard', JSON.stringify(items)); + renderMoodboard(); + }; + container.appendChild(addCard); +} + +let moodUploadTarget = -1; +let moodDragIndex = -1; +document.addEventListener('DOMContentLoaded', () => { + const moodInput = document.getElementById('mood-file-input'); + if (moodInput) moodInput.addEventListener('change', function() { + const file = this.files[0]; + if (!file || moodUploadTarget < 0) return; + const reader = new FileReader(); + reader.onload = (e) => { + const img = new Image(); + img.onload = async () => { + const maxW = 1200; + let w = img.width, h = img.height; + if (w > maxW) { h = Math.round(h * maxW / w); w = maxW; } + const canvas = document.createElement('canvas'); + canvas.width = w; canvas.height = h; + canvas.getContext('2d').drawImage(img, 0, 0, w, h); + const dataUrl = canvas.toDataURL('image/jpeg', 0.8); + + try { + const res = await fetch('/api/mood-upload', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ dataUrl, index: moodUploadTarget }), + }); + const data = await res.json(); + if (data.path) { + const row = visionData['moodboard']; + let items = []; + try { items = JSON.parse(row?.Contenu || '[]'); } catch { items = []; } + if (items[moodUploadTarget]) { + items[moodUploadTarget].image = data.path; + items[moodUploadTarget].posX = 50; + items[moodUploadTarget].posY = 50; + items[moodUploadTarget].zoom = 100; + saveVisionSection('moodboard', JSON.stringify(items)); + renderMoodboard(); + } + } + } catch (err) { console.error('Mood upload error:', err); } + }; + img.src = e.target.result; + }; + reader.readAsDataURL(file); + this.value = ''; + }); +}); + +function removeMoodItem(index) { + const row = visionData['moodboard']; + let items = []; + try { items = JSON.parse(row?.Contenu || '[]'); } catch { return; } + items.splice(index, 1); + saveVisionSection('moodboard', JSON.stringify(items)); + renderMoodboard(); +} + +// ── Hero image with drag-to-reposition ── +let heroMeta = { posY: 50, height: 400 }; // posY in %, height in px + +function applyHeroImage(dataUrl) { + const banner = document.getElementById('hero-banner'); + if (dataUrl) { + banner.style.backgroundImage = `url(${dataUrl})`; + banner.classList.add('has-image'); + } else { + banner.style.backgroundImage = ''; + banner.classList.remove('has-image'); + } +} + +function applyHeroMeta() { + const banner = document.getElementById('hero-banner'); + banner.style.backgroundPosition = `center ${heroMeta.posY}%`; + banner.style.height = heroMeta.height + 'px'; +} + +function saveHeroMeta() { + saveVisionSection('hero-meta', JSON.stringify({ posY: heroMeta.posY, height: heroMeta.height })); +} + +// Drag to reposition +(function() { + const banner = document.getElementById('hero-banner'); + let dragging = false, startY = 0, startPos = 0; + + banner.addEventListener('mousedown', (e) => { + if (!banner.classList.contains('has-image')) return; + if (e.target.closest('[contenteditable]') || e.target.closest('.hero-controls') || e.target.closest('button')) return; + dragging = true; + startY = e.clientY; + startPos = heroMeta.posY; + banner.classList.add('dragging'); + e.preventDefault(); + }); + + window.addEventListener('mousemove', (e) => { + if (!dragging) return; + const delta = e.clientY - startY; + const sensitivity = 0.15; + heroMeta.posY = Math.max(0, Math.min(100, startPos + delta * sensitivity)); + banner.style.backgroundPosition = `center ${heroMeta.posY}%`; + }); + + window.addEventListener('mouseup', () => { + if (!dragging) return; + dragging = false; + banner.classList.remove('dragging'); + saveHeroMeta(); + }); +})(); + +async function uploadHeroImage(input) { + const file = input.files[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = async (e) => { + const img = new Image(); + img.onload = async () => { + const maxW = 1600; + let w = img.width, h = img.height; + if (w > maxW) { h = Math.round(h * maxW / w); w = maxW; } + const canvas = document.createElement('canvas'); + canvas.width = w; canvas.height = h; + canvas.getContext('2d').drawImage(img, 0, 0, w, h); + const dataUrl = canvas.toDataURL('image/jpeg', 0.85); + + // Upload file to server (too large for NocoDB LongText) + try { + const res = await fetch('/api/hero-upload', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ dataUrl }), + }); + const data = await res.json(); + if (data.path) { + applyHeroImage(data.path); + heroMeta.posY = 50; + applyHeroMeta(); + saveVisionSection('hero-image', data.path); + saveHeroMeta(); + } + } catch (err) { + console.error('Hero upload error:', err); + } + }; + img.src = e.target.result; + }; + reader.readAsDataURL(file); + input.value = ''; +} + +// ── Load Vision data ── +async function loadVision() { + try { + const res = await fetch('/api/vision'); + if (res.status === 401) return; + const data = await res.json(); + const rows = data.list || []; + + // Index by section + visionData = {}; + rows.forEach(r => { visionData[r.Section] = r; }); + + // Populate text fields + for (const [section, elId] of Object.entries(FIELD_MAP)) { + const el = document.getElementById(elId); + if (!el) continue; + const contenu = visionData[section]?.Contenu || ''; + el.textContent = contenu || PLACEHOLDERS[section] || ''; + setupEditable(elId, section); + } + + // Hero image (base64 brut) + meta (position/hauteur séparées) + applyHeroImage(visionData['hero-image']?.Contenu || ''); + try { + const meta = JSON.parse(visionData['hero-meta']?.Contenu || '{}'); + heroMeta.posY = meta.posY ?? 50; + heroMeta.height = meta.height ?? 400; + } catch { /* defaults */ } + applyHeroMeta(); + + // Render structured sections + renderTags(); + renderList('on-veut', 'v-on-veut', 'yes'); + renderList('on-ne-veut-pas', 'v-on-ne-veut-pas', 'no'); + renderMoodboard(); + } catch (e) { + console.error('Vision load error:', e); + } +} + +loadVision(); + +// ══════════════════════════════════════════════════════ +// ══ PERSONNAGES ══ +// ══════════════════════════════════════════════════════ + +let personnages = []; +let editingPersoId = null; +let pendingPersoImage = null; + +async function loadPersonnages() { + try { + const res = await fetch('/api/personnages'); + if (res.status === 401) return; + const data = await res.json(); + personnages = data.list || []; + renderPersonnages(); + } catch (e) { console.error('Personnages load error:', e); } +} + +function renderPersonnages() { + const grid = document.getElementById('perso-grid'); + grid.textContent = ''; + personnages.forEach(p => { + const card = document.createElement('div'); + card.className = 'perso-card'; + card.onclick = () => openPersoEdit(p.Id); + + // Portrait + if (p.Image) { + const img = document.createElement('img'); + img.className = 'perso-card-portrait'; + img.src = p.Image; + img.alt = p.Nom || ''; + card.appendChild(img); + } + + const body = document.createElement('div'); + body.className = 'perso-card-body'; + + // Nom + const nameEl = document.createElement('div'); + nameEl.className = 'perso-card-name'; + nameEl.textContent = p.Nom || ''; + body.appendChild(nameEl); + + // Rôle + if (p.Role) { + const roleEl = document.createElement('div'); + roleEl.className = 'perso-card-role'; + roleEl.textContent = p.Role; + body.appendChild(roleEl); + } + + // Description (tronquée) + if (p.Description) { + const descEl = document.createElement('div'); + descEl.className = 'perso-card-desc perso-card-text'; + descEl.appendChild(createSanitizedFragment(renderMarkdown(p.Description))); + body.appendChild(descEl); + } + + // Tags + const tagList = (p.Tags || '').split(',').filter(t => t.trim()); + if (tagList.length) { + const tagsEl = document.createElement('div'); + tagsEl.className = 'perso-card-tags'; + tagList.forEach(t => { + const badge = document.createElement('span'); + badge.className = `card-badge ${tagBadgeClass(t.trim())}`; + badge.textContent = t.trim(); + tagsEl.appendChild(badge); + }); + body.appendChild(tagsEl); + } + + // Compteur scènes + const sceneCount = records.filter(r => + r.Personnage && r.Personnage.split(',').map(s => s.trim()).includes(p.Nom) + ).length; + if (sceneCount) { + const countEl = document.createElement('div'); + countEl.className = 'perso-card-scene-count'; + countEl.textContent = `${sceneCount} scène${sceneCount > 1 ? 's' : ''}`; + body.appendChild(countEl); + } + + card.appendChild(body); + grid.appendChild(card); + }); +} + +function getLinkedScenes(nom) { + return records.filter(r => + r.Personnage && r.Personnage.split(',').map(s => s.trim()).includes(nom) + ).sort((a, b) => { + const ai = ACTE_ORDER.indexOf(a.Acte), bi = ACTE_ORDER.indexOf(b.Acte); + if (ai !== bi) return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi); + return (a.Ordre || 0) - (b.Ordre || 0); + }); +} + +function buildModalScenes(nom) { + const container = document.getElementById('fp-scenes-container'); + container.textContent = ''; + const scenes = getLinkedScenes(nom); + if (!scenes.length) return; + + const wrapper = document.createElement('div'); + wrapper.className = 'perso-modal-scenes'; + const title = document.createElement('div'); + title.className = 'perso-modal-scenes-title'; + title.textContent = `Scènes (${scenes.length})`; + wrapper.appendChild(title); + + // Group by acte + const grouped = {}; + scenes.forEach(s => { + const acte = s.Acte || 'Sans acte'; + if (!grouped[acte]) grouped[acte] = []; + grouped[acte].push(s); + }); + + ACTE_ORDER.forEach(acte => { + if (!grouped[acte] || !grouped[acte].length) return; + const acteTitle = document.createElement('div'); + acteTitle.className = 'perso-modal-scenes-acte'; + acteTitle.textContent = acte; + wrapper.appendChild(acteTitle); + const list = document.createElement('div'); + list.className = 'perso-modal-scenes-list'; + grouped[acte].forEach(scene => { + const link = document.createElement('a'); + link.textContent = scene.Title; + link.onclick = (e) => { e.stopPropagation(); closePersoModal(); switchView('frise'); setTimeout(() => openEdit(scene.Id), 200); }; + list.appendChild(link); + }); + wrapper.appendChild(list); + }); + + container.appendChild(wrapper); +} + +function openPersoModal() { + editingPersoId = null; + pendingPersoImage = null; + document.getElementById('perso-modal-label').textContent = 'Nouveau personnage'; + document.getElementById('fp-nom').value = ''; + document.getElementById('fp-role').value = ''; + document.getElementById('fp-desc').value = ''; + document.getElementById('fp-origines').value = ''; + document.getElementById('fp-backstory').value = ''; + document.getElementById('fp-tags').value = ''; + document.getElementById('fp-image-input').value = ''; + document.getElementById('fp-image-preview').innerHTML = ''; + document.getElementById('fp-scenes-container').textContent = ''; + document.getElementById('btn-delete-perso').style.display = 'none'; + document.getElementById('perso-modal').classList.add('open'); +} + +function openPersoEdit(id) { + const p = personnages.find(x => x.Id === id); + if (!p) return; + editingPersoId = id; + pendingPersoImage = null; + document.getElementById('perso-modal-label').textContent = 'Modifier'; + document.getElementById('fp-nom').value = p.Nom || ''; + document.getElementById('fp-role').value = p.Role || ''; + document.getElementById('fp-desc').value = p.Description || ''; + document.getElementById('fp-origines').value = p.Origines || ''; + document.getElementById('fp-backstory').value = p.Backstory || ''; + document.getElementById('fp-tags').value = p.Tags || ''; + document.getElementById('fp-image-input').value = ''; + document.getElementById('fp-image-preview').innerHTML = p.Image ? `` : ''; + buildModalScenes(p.Nom); + document.getElementById('btn-delete-perso').style.display = 'block'; + document.getElementById('perso-modal').classList.add('open'); +} + +function closePersoModal() { + document.getElementById('perso-modal').classList.remove('open'); + editingPersoId = null; + pendingPersoImage = null; +} + +function previewPersoImage(input) { + const file = input.files[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = e => { + pendingPersoImage = e.target.result; + document.getElementById('fp-image-preview').innerHTML = ``; + }; + reader.readAsDataURL(file); +} + +async function savePerso() { + let imagePath = null; + if (pendingPersoImage) { + const upRes = await fetch('/api/ressource-upload', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ dataUrl: pendingPersoImage }), + }); + if (upRes.ok) { + const upData = await upRes.json(); + imagePath = upData.path; + } + } + + const data = { + Nom: document.getElementById('fp-nom').value, + Role: document.getElementById('fp-role').value || null, + Description: document.getElementById('fp-desc').value || null, + Origines: document.getElementById('fp-origines').value || null, + Backstory: document.getElementById('fp-backstory').value || null, + Tags: document.getElementById('fp-tags').value || null, + }; + if (imagePath) data.Image = imagePath; + + if (editingPersoId) { + await fetch(`/api/personnages/${editingPersoId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }); + } else { + await fetch('/api/personnages', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }); + } + closePersoModal(); + await loadPersonnages(); +} + +async function deletePerso() { + if (!editingPersoId || !confirm('Supprimer ce personnage ?')) return; + await fetch(`/api/personnages/${editingPersoId}`, { method: 'DELETE' }); + closePersoModal(); + await loadPersonnages(); +} + +document.getElementById('perso-modal').addEventListener('click', e => { + if (e.target === document.getElementById('perso-modal')) closePersoModal(); +}); + +// ══════════════════════════════════════════════════════ +// ══ RESSOURCES ══ +// ══════════════════════════════════════════════════════ + +let ressources = []; +let editingResId = null; +let pendingResImage = null; // dataUrl for new upload + +function videoThumbnail(url) { + if (!url) return null; + // YouTube: various URL formats + let m = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/); + if (m) return `https://img.youtube.com/vi/${m[1]}/hqdefault.jpg`; + // Vimeo + m = url.match(/vimeo\.com\/(\d+)/); + if (m) return `https://vumbnail.com/${m[1]}.jpg`; + return null; +} + +function extractDomain(url) { + try { return new URL(url).hostname.replace('www.', ''); } catch { return url; } +} + +async function loadRessources() { + try { + const res = await fetch('/api/ressources'); + if (res.status === 401) return; + const data = await res.json(); + ressources = data.list || []; + populateResCatFilter(); + renderRessources(); + } catch (e) { console.error('Ressources load error:', e); } +} + +function populateResCatFilter() { + const catSet = new Set(); + ressources.forEach(r => { if (r.Categorie) catSet.add(r.Categorie); }); + const sel = document.getElementById('res-filter-cat'); + const cur = sel.value; + sel.innerHTML = ''; + [...catSet].sort().forEach(c => { + const opt = document.createElement('option'); + opt.value = c; opt.textContent = c; + sel.appendChild(opt); + }); + sel.value = cur; +} + +function renderRessources() { + const grid = document.getElementById('res-grid'); + grid.innerHTML = ''; + const filterCat = document.getElementById('res-filter-cat')?.value || ''; + const filterSearch = (document.getElementById('res-filter-search')?.value || '').toLowerCase().trim(); + + ressources.forEach(r => { + if (filterCat && r.Categorie !== filterCat) return; + if (filterSearch) { + const hay = [r.Titre, r.Description, r.Tags, r.Categorie, r.Lien].filter(Boolean).join(' ').toLowerCase(); + if (!hay.includes(filterSearch)) return; + } + + const card = document.createElement('div'); + card.className = 'res-card'; + card.onclick = () => openResEdit(r.Id); + + // Image: uploaded > video thumbnail > nothing + const imgSrc = r.Image || videoThumbnail(r.Lien); + if (imgSrc) { + const img = document.createElement('img'); + img.className = 'res-card-img'; + img.src = imgSrc; + img.alt = ''; + card.appendChild(img); + } + + const body = document.createElement('div'); + body.className = 'res-card-body'; + + const titleEl = document.createElement('div'); + titleEl.className = 'res-card-title'; + titleEl.textContent = r.Titre || ''; + body.appendChild(titleEl); + + if (r.Categorie) { + const catEl = document.createElement('div'); + catEl.className = 'res-card-cat'; + catEl.textContent = r.Categorie; + body.appendChild(catEl); + } + + if (r.Description) { + const descEl = document.createElement('div'); + descEl.className = 'res-card-desc'; + descEl.textContent = r.Description; + body.appendChild(descEl); + } + + // Lien cliquable + if (r.Lien) { + const linkDiv = document.createElement('div'); + linkDiv.className = 'res-card-link'; + linkDiv.innerHTML = ''; + const a = document.createElement('a'); + a.href = r.Lien; + a.target = '_blank'; + a.rel = 'noopener'; + a.textContent = extractDomain(r.Lien); + a.onclick = (e) => e.stopPropagation(); + linkDiv.appendChild(a); + body.appendChild(linkDiv); + } + + const tags = (r.Tags || '').split(',').filter(t => t.trim()); + if (tags.length) { + const tagsEl = document.createElement('div'); + tagsEl.className = 'res-card-tags'; + tags.forEach(t => { + const badge = document.createElement('span'); + badge.className = 'card-badge badge-tag'; + badge.style.cssText = 'font-size:11px;padding:2px 6px;'; + badge.textContent = t.trim(); + tagsEl.appendChild(badge); + }); + body.appendChild(tagsEl); + } + + card.appendChild(body); + grid.appendChild(card); + }); +} + +function updateResUrlLink(url) { + const container = document.getElementById('fr-url-link'); + container.innerHTML = ''; + if (url && url.startsWith('http')) { + const a = document.createElement('a'); + a.href = url; + a.target = '_blank'; + a.rel = 'noopener'; + a.textContent = 'Ouvrir le lien'; + container.appendChild(a); + } +} + +function openResModal() { + editingResId = null; + pendingResImage = null; + document.getElementById('res-modal-label').textContent = 'Nouvelle ressource'; + document.getElementById('fr-titre').value = ''; + document.getElementById('fr-cat').value = ''; + document.getElementById('fr-tags').value = ''; + document.getElementById('fr-url').value = ''; + document.getElementById('fr-desc').value = ''; + document.getElementById('fr-image-input').value = ''; + document.getElementById('fr-image-preview').innerHTML = ''; + updateResUrlLink(''); + document.getElementById('btn-delete-res').style.display = 'none'; + document.getElementById('res-modal').classList.add('open'); +} + +function openResEdit(id) { + const r = ressources.find(x => x.Id === id); + if (!r) return; + editingResId = id; + pendingResImage = null; + document.getElementById('res-modal-label').textContent = 'Modifier'; + document.getElementById('fr-titre').value = r.Titre || ''; + document.getElementById('fr-cat').value = r.Categorie || ''; + document.getElementById('fr-tags').value = r.Tags || ''; + document.getElementById('fr-url').value = r.Lien || ''; + document.getElementById('fr-desc').value = r.Description || ''; + document.getElementById('fr-image-input').value = ''; + document.getElementById('fr-image-preview').innerHTML = r.Image ? `` : ''; + updateResUrlLink(r.Lien || ''); + document.getElementById('btn-delete-res').style.display = 'block'; + document.getElementById('res-modal').classList.add('open'); +} + +function closeResModal() { + document.getElementById('res-modal').classList.remove('open'); + editingResId = null; + pendingResImage = null; +} + +function previewResImage(input) { + const file = input.files[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = e => { + pendingResImage = e.target.result; + document.getElementById('fr-image-preview').innerHTML = ``; + }; + reader.readAsDataURL(file); +} + +async function saveRes() { + let imagePath = null; + + // Upload image if new one selected + if (pendingResImage) { + const upRes = await fetch('/api/ressource-upload', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ dataUrl: pendingResImage }), + }); + if (upRes.ok) { + const upData = await upRes.json(); + imagePath = upData.path; + } + } + + const data = { + Titre: document.getElementById('fr-titre').value, + Categorie: document.getElementById('fr-cat').value || null, + Tags: document.getElementById('fr-tags').value || null, + Lien: document.getElementById('fr-url').value || null, + Description: document.getElementById('fr-desc').value || null, + }; + if (imagePath) data.Image = imagePath; + + if (editingResId) { + await fetch(`/api/ressources/${editingResId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }); + } else { + await fetch('/api/ressources', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }); + } + closeResModal(); + await loadRessources(); +} + +async function deleteRes() { + if (!editingResId || !confirm('Supprimer cette ressource ?')) return; + await fetch(`/api/ressources/${editingResId}`, { method: 'DELETE' }); + closeResModal(); + await loadRessources(); +} + +document.getElementById('res-modal').addEventListener('click', e => { + if (e.target === document.getElementById('res-modal')) closeResModal(); +}); + +// ══════════════════════════════════════════════════════ +// ══ RÉDACTION — Binder + TipTap Editor ══ +// ══════════════════════════════════════════════════════ + +let tiptapEditor = null; +let currentSceneId = null; +let saveTimeout = null; +let wordCounts = {}; // sceneId → word count (cached) + +const ACTE_ORDER = ["Prologue", 'Acte 1', 'Acte 2', 'Acte 3', "Épilogue"]; + +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] = []; }); + + const redPerso = document.getElementById('red-filter-perso')?.value || ''; + const redTag = document.getElementById('red-filter-tag')?.value || ''; + const redSearch = (document.getElementById('red-filter-search')?.value || '').toLowerCase().trim(); + + (records || []).forEach(r => { + const acte = r.Acte; + if (!acte || acte === 'En attente') return; + if (redPerso && !(r.Personnage || '').split(',').map(s => s.trim()).includes(redPerso)) return; + if (redTag && !(r.Tags || '').split(',').map(s => s.trim()).includes(redTag)) return; + if (redSearch) { + const haystack = [r.Title, r['Résumé'], r.Personnage].filter(Boolean).join(' ').toLowerCase(); + if (!haystack.includes(redSearch)) return; + } + if (grouped[acte]) grouped[acte].push(r); + }); + + binder.innerHTML = ''; + + // Legend at top + const legend = document.createElement('div'); + legend.className = 'binder-legend'; + const legendTitle = document.createElement('div'); + legendTitle.className = 'binder-legend-title'; + legendTitle.textContent = 'Tags statut'; + legend.appendChild(legendTitle); + Object.entries(STATUS_COLORS).forEach(([label, color]) => { + const item = document.createElement('div'); + item.className = 'binder-legend-item'; + const dot = document.createElement('span'); + dot.className = 'binder-legend-dot'; + dot.style.background = color; + const txt = document.createElement('span'); + txt.textContent = label; + item.appendChild(dot); + item.appendChild(txt); + legend.appendChild(item); + }); + binder.appendChild(legend); + + 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 sceneTags = (scene.Tags || '').split(',').map(t => t.trim()).filter(Boolean); + const statusDots = sceneTags + .filter(t => STATUS_COLORS[t]) + .map(t => ``) + .join(''); + const wc = wordCounts[scene.Id] || 0; + totalWords += wc; + el.innerHTML = `${statusDots || ''}${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.Tags) meta.push(scene.Tags); + if (scene.Personnage) meta.push(scene.Personnage); + document.getElementById('editor-meta').textContent = meta.join(' · '); + document.getElementById('btn-history-redaction').style.display = ''; + + // Panneau fiche (intention) + buildFichePanel(scene); + + // 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(''); + } +} + +// ── Panneau fiche (intention) ── +let ficheOpen = false; +try { ficheOpen = localStorage.getItem('tdb-fiche-open') === '1'; } catch(e) {} + +function toggleFiche() { + ficheOpen = !ficheOpen; + document.getElementById('editor-fiche-content').classList.toggle('open', ficheOpen); + document.getElementById('fiche-toggle-icon').classList.toggle('open', ficheOpen); + try { localStorage.setItem('tdb-fiche-open', ficheOpen ? '1' : '0'); } catch(e) {} +} + +function buildFichePanel(scene) { + const container = document.getElementById('editor-fiche-content'); + container.textContent = ''; + + const fields = [ + { label: 'Résumé', value: scene['Résumé'] }, + { label: 'Intérêt narratif', value: scene['Intérêt narratif'] }, + { label: 'Implications', value: scene.Implications }, + { label: 'Origine', value: scene.Origine }, + { label: 'Lien rédaction', value: scene['Lien rédaction'] }, + ]; + + let hasContent = false; + fields.forEach(f => { + if (!f.value) return; + hasContent = true; + const field = document.createElement('div'); + field.className = 'editor-fiche-field'; + const label = document.createElement('div'); + label.className = 'editor-fiche-field-label'; + label.textContent = f.label; + field.appendChild(label); + const val = document.createElement('div'); + val.className = 'editor-fiche-field-value'; + val.appendChild(createSanitizedFragment(renderMarkdown(f.value))); + field.appendChild(val); + container.appendChild(field); + }); + + // Tags + const tagList = (scene.Tags || '').split(',').map(t => t.trim()).filter(Boolean); + if (tagList.length) { + hasContent = true; + const field = document.createElement('div'); + field.className = 'editor-fiche-field'; + const label = document.createElement('div'); + label.className = 'editor-fiche-field-label'; + label.textContent = 'Tags'; + field.appendChild(label); + const tagsEl = document.createElement('div'); + tagsEl.className = 'editor-fiche-tags'; + tagList.forEach(t => { + const badge = document.createElement('span'); + badge.className = `card-badge ${tagBadgeClass(t)}`; + badge.textContent = t; + tagsEl.appendChild(badge); + }); + field.appendChild(tagsEl); + container.appendChild(field); + } + + // Personnages + const persos = (scene.Personnage || '').split(',').map(p => p.trim()).filter(Boolean); + if (persos.length) { + hasContent = true; + const field = document.createElement('div'); + field.className = 'editor-fiche-field'; + const label = document.createElement('div'); + label.className = 'editor-fiche-field-label'; + label.textContent = 'Personnages'; + field.appendChild(label); + const persosEl = document.createElement('div'); + persosEl.className = 'editor-fiche-persos'; + persos.forEach(p => { + const badge = document.createElement('span'); + badge.className = 'card-badge badge-perso'; + badge.textContent = p; + persosEl.appendChild(badge); + }); + field.appendChild(persosEl); + container.appendChild(field); + } + + // Commentaires de la fiche + const comments = parseComments(scene.Commentaires); + if (comments.length) { + hasContent = true; + const field = document.createElement('div'); + field.className = 'editor-fiche-field'; + const label = document.createElement('div'); + label.className = 'editor-fiche-field-label'; + label.textContent = `Commentaires (${comments.length})`; + field.appendChild(label); + const list = document.createElement('div'); + comments.forEach(c => { + const item = document.createElement('div'); + item.style.cssText = 'margin-bottom:6px;'; + const author = document.createElement('span'); + author.style.cssText = 'font-weight:600;font-size:12px;'; + author.textContent = (c.auteur === 'M' ? 'Myriam' : 'Etienne') + ' — '; + item.appendChild(author); + const text = document.createElement('span'); + text.textContent = c.texte || ''; + item.appendChild(text); + list.appendChild(item); + }); + field.appendChild(list); + container.appendChild(field); + } + + // Afficher/masquer le panneau selon l'état mémorisé + const ficheEl = document.getElementById('editor-fiche'); + ficheEl.style.display = hasContent ? '' : 'none'; + container.classList.toggle('open', ficheOpen && hasContent); + document.getElementById('fiche-toggle-icon').classList.toggle('open', ficheOpen && hasContent); +} + +function initEditor(html) { + // Destroy previous editor + if (tiptapEditor) { tiptapEditor.destroy(); tiptapEditor = null; } + + const Editor = window.TiptapEditor; + const StarterKit = window.TiptapStarterKit; + const CommentMark = window.TiptapComment; + + 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; + } + + const extensions = [StarterKit]; + if (CommentMark) extensions.push(CommentMark); + + tiptapEditor = new Editor({ + element: document.getElementById('editor-content'), + extensions, + content: html || '

', + onUpdate: () => { + clearTimeout(saveTimeout); + saveTimeout = setTimeout(saveContent, 1500); + updateWordCount(); + }, + }); + + // Listen for clicks on comment marks + document.getElementById('editor-content').addEventListener('click', (e) => { + const mark = e.target.closest('.comment-mark'); + if (mark) showCommentPopup(mark, e); + }); + + 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); + }); + + // Separator + Comment button + const sep = document.createElement('span'); + sep.className = 'toolbar-sep'; + bar.appendChild(sep); + + const commentBtn = document.createElement('button'); + commentBtn.innerHTML = 'Commenter'; + commentBtn.title = 'Ajouter un commentaire sur la sélection'; + commentBtn.style.cssText = 'font-size:13px;'; + commentBtn.onclick = () => addComment(); + bar.appendChild(commentBtn); + + // 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(); + +// ── Comments ── +function addComment() { + if (!tiptapEditor) return; + const { from, to } = tiptapEditor.state.selection; + if (from === to) { alert('Sélectionnez du texte pour commenter.'); return; } + + const author = prompt('Auteur (E ou M) :', 'E'); + if (!author) return; + const text = prompt('Commentaire :'); + if (!text) return; + + const now = new Date().toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' }); + + tiptapEditor.chain().focus() + .setMark('comment', { text, author: author.toUpperCase(), date: now }) + .run(); +} + +let activeCommentMark = null; + +function showCommentPopup(markEl, event) { + activeCommentMark = markEl; + const popup = document.getElementById('comment-popup'); + const author = markEl.getAttribute('data-author') || 'E'; + const authorName = author === 'M' ? 'Myriam' : 'Etienne'; + + document.getElementById('comment-popup-author').textContent = authorName; + document.getElementById('comment-popup-author').className = `comment-popup-author ${author.toLowerCase()}`; + document.getElementById('comment-popup-date').textContent = markEl.getAttribute('data-date') || ''; + document.getElementById('comment-popup-text').textContent = markEl.getAttribute('data-comment') || ''; + + // Position near click + const rect = markEl.getBoundingClientRect(); + popup.style.display = 'block'; + popup.style.top = (rect.bottom + 8) + 'px'; + popup.style.left = Math.min(rect.left, window.innerWidth - 340) + 'px'; +} + +function closeCommentPopup() { + document.getElementById('comment-popup').style.display = 'none'; + activeCommentMark = null; +} + +function deleteComment() { + if (!tiptapEditor || !activeCommentMark) return; + // Find the position of this comment span in the document + const commentText = activeCommentMark.getAttribute('data-comment'); + const commentDate = activeCommentMark.getAttribute('data-date'); + const doc = tiptapEditor.state.doc; + let markFrom = null, markTo = null; + doc.descendants((node, pos) => { + if (markFrom !== null) return false; + if (node.isText) { + const mark = node.marks.find(m => m.type.name === 'comment' && m.attrs.text === commentText && m.attrs.date === commentDate); + if (mark) { + markFrom = pos; + markTo = pos + node.nodeSize; + } + } + }); + if (markFrom !== null) { + tiptapEditor.chain().focus().setTextSelection({ from: markFrom, to: markTo }).unsetMark('comment').run(); + } + closeCommentPopup(); + clearTimeout(saveTimeout); + saveTimeout = setTimeout(saveContent, 500); +} + +// Close popup on click outside +document.addEventListener('click', (e) => { + const popup = document.getElementById('comment-popup'); + if (popup.style.display === 'block' && !popup.contains(e.target) && !e.target.closest('.comment-mark')) { + closeCommentPopup(); + } +}); +// ── Version history ── +let versionType = null; +let versionRecordId = null; +let versionList = []; +let selectedVersionId = null; + +async function openVersionPanel(type, recordId) { + if (!recordId) return; + versionType = type; + versionRecordId = recordId; + selectedVersionId = null; + document.getElementById('version-panel-title').textContent = type === 'frise' ? 'Historique — Fiche' : 'Historique — Texte'; + document.getElementById('version-list').innerHTML = '
Chargement...
'; + document.getElementById('version-preview').style.display = 'none'; + document.getElementById('version-actions').style.display = 'none'; + document.getElementById('version-panel').classList.add('open'); + + try { + const r = await fetch(`/api/versions/${type}/${recordId}`); + const data = await r.json(); + versionList = data.list || []; + renderVersionList(); + } catch (e) { + document.getElementById('version-list').innerHTML = '
Erreur de chargement
'; + } +} + +function closeVersionPanel() { + document.getElementById('version-panel').classList.remove('open'); + versionType = null; + versionRecordId = null; + selectedVersionId = null; +} + +function renderVersionList() { + const container = document.getElementById('version-list'); + if (versionList.length === 0) { + container.innerHTML = '
Aucune version enregistrée pour le moment.
Les versions seront créées automatiquement à chaque modification.
'; + return; + } + container.innerHTML = ''; + for (const v of versionList) { + const div = document.createElement('div'); + div.className = 'version-item'; + div.dataset.id = v.Id; + const date = new Date(v.CreatedAt); + const dateStr = date.toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit' }); + let preview = ''; + try { + const snap = JSON.parse(v.Snapshot); + if (versionType === 'frise') { + preview = [snap.Title, snap['Résumé']].filter(Boolean).join(' — '); + } else { + const tmp = document.createElement('div'); + tmp.innerHTML = snap.Texte || ''; + preview = tmp.textContent.substring(0, 150); + } + } catch (e) { preview = ''; } + const label = v.Label || ''; + div.innerHTML = `
${dateStr}
` + + (label ? `
${label.replace(/` : '') + + (preview ? `
${preview.replace(/` : ''); + div.onclick = () => selectVersion(v.Id); + container.appendChild(div); + } +} + +function selectVersion(id) { + selectedVersionId = id; + document.querySelectorAll('.version-item').forEach(el => el.classList.toggle('selected', Number(el.dataset.id) === id)); + const v = versionList.find(x => x.Id === id); + if (!v) return; + try { + const snap = JSON.parse(v.Snapshot); + const previewPane = document.getElementById('version-preview'); + const content = document.getElementById('version-preview-content'); + if (versionType === 'frise') { + const parts = []; + if (snap.Title) parts.push(`${snap.Title.replace(/`); + if (snap.Acte) parts.push(`${snap.Acte}`); + if (snap['Résumé']) parts.push(snap['Résumé'].replace(/Intérêt : ' + snap['Intérêt narratif'].replace(/Tags : ' + snap.Tags.replace(/'); + content.innerHTML = parts.join('
'); + } else { + content.innerHTML = DOMPurify.sanitize(snap.Texte || 'Texte vide'); + } + previewPane.style.display = ''; + document.getElementById('version-actions').style.display = ''; + } catch (e) { + document.getElementById('version-preview').style.display = 'none'; + } +} + +async function restoreVersion() { + if (!selectedVersionId) return; + const isArchive = versionType === 'archive'; + const msg = isArchive + ? 'Restaurer cette fiche supprimée ? Elle sera recréée dans la frise.' + : 'Restaurer cette version ? L\u2019état actuel sera sauvegardé avant la restauration.'; + if (!confirm(msg)) return; + try { + if (isArchive) { + // Re-create the card from snapshot + const v = versionList.find(x => x.Id === selectedVersionId); + if (!v) return; + const snap = JSON.parse(v.Snapshot); + const { Title, Acte, Ordre, Personnage, Tags, Notes } = snap; + const data = { Title, Acte: 'En attente', Ordre: 999, Personnage, Tags, Notes, 'Résumé': snap['Résumé'], 'Intérêt narratif': snap['Intérêt narratif'], Implications: snap.Implications, Origine: snap.Origine, 'Lien rédaction': snap['Lien rédaction'] }; + const r = await fetch('/api/records', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); + if (r.ok) { + closeVersionPanel(); + await loadData(); + } else { + alert('Erreur lors de la restauration'); + } + } else { + const r = await fetch(`/api/versions/${selectedVersionId}/restore`, { method: 'POST' }); + if (r.ok) { + closeVersionPanel(); + if (versionType === 'frise') { + await loadData(); + if (editingId) openEdit(editingId); + } else if (versionType === 'redaction' && currentSceneId) { + const scene = records.find(rec => rec.Id === currentSceneId); + if (scene) openScene(scene); + } + } else { + alert('Erreur lors de la restauration'); + } + } + } catch (e) { + alert('Erreur : ' + e.message); + } +} + +// ── Archives (deleted cards) ── +async function openArchives() { + versionType = 'archive'; + versionRecordId = null; + selectedVersionId = null; + document.getElementById('version-panel-title').textContent = 'Archives — Fiches supprimées'; + document.getElementById('version-list').innerHTML = '
Chargement...
'; + document.getElementById('version-preview').style.display = 'none'; + document.getElementById('version-actions').style.display = 'none'; + document.getElementById('version-panel').classList.add('open'); + + try { + const r = await fetch('/api/archives'); + const data = await r.json(); + versionList = data.list || []; + renderArchiveList(); + } catch (e) { + document.getElementById('version-list').innerHTML = '
Erreur de chargement
'; + } +} + +function renderArchiveList() { + const container = document.getElementById('version-list'); + if (versionList.length === 0) { + container.innerHTML = '
Aucune fiche supprimée.
'; + return; + } + container.innerHTML = ''; + for (const v of versionList) { + const div = document.createElement('div'); + div.className = 'version-item'; + div.dataset.id = v.Id; + const date = new Date(v.CreatedAt); + const dateStr = date.toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit' }); + let title = '', preview = ''; + try { + const snap = JSON.parse(v.Snapshot); + title = snap.Title || snap.Titre || 'Sans titre'; + preview = [snap.Acte, snap['Résumé']].filter(Boolean).join(' — '); + } catch (e) { title = 'Fiche'; } + div.innerHTML = `
${dateStr}
` + + `
${title.replace(/` + + (preview ? `
${preview.replace(/` : ''); + div.onclick = () => selectArchive(v.Id); + container.appendChild(div); + } +} + +function selectArchive(id) { + selectedVersionId = id; + versionType = 'archive'; + document.querySelectorAll('.version-item').forEach(el => el.classList.toggle('selected', Number(el.dataset.id) === id)); + const v = versionList.find(x => x.Id === id); + if (!v) return; + try { + const snap = JSON.parse(v.Snapshot); + const content = document.getElementById('version-preview-content'); + const parts = []; + if (snap.Title) parts.push(`${snap.Title.replace(/`); + if (snap.Acte) parts.push(`${snap.Acte}`); + if (snap.Personnage) parts.push('Personnages : ' + snap.Personnage.replace(/Intérêt : ' + snap['Intérêt narratif'].replace(/Tags : ' + snap.Tags.replace(/'); + if (snap.Notes) parts.push('Notes : ' + snap.Notes.replace(/'); + document.getElementById('version-preview').style.display = ''; + document.getElementById('version-actions').style.display = ''; + } catch (e) { + document.getElementById('version-preview').style.display = 'none'; + } +} + +// Close popup on version panel click outside +document.addEventListener('click', (e) => { + const panel = document.getElementById('version-panel'); + if (panel.classList.contains('open') && !panel.contains(e.target) && !e.target.closest('.btn-history')) { + closeVersionPanel(); + } +}); diff --git a/public/index.html b/public/index.html index 497e075..c5a025d 100644 --- a/public/index.html +++ b/public/index.html @@ -39,523 +39,7 @@ window.TiptapStarterKit = StarterKit; window.TiptapComment = Comment; window.dispatchEvent(new Event('tiptap-loaded')); - +
@@ -877,2419 +361,7 @@ window.dispatchEvent(new Event('tiptap-loaded'));
- +
diff --git a/public/styles.css b/public/styles.css new file mode 100644 index 0000000..af8030e --- /dev/null +++ b/public/styles.css @@ -0,0 +1,515 @@ + * { margin: 0; padding: 0; box-sizing: border-box; } + + /* ── Light theme (default) ── */ + :root { + --bg: #e8e0d4; + --bg-alt: #ddd5c8; + --bg-card: #f0e9de; + --bg-input: #e8e0d4; + --border: #cdc0b0; + --border-light: #ddd6ca; + --text: #2c1810; + --text-muted: #6e5d4e; + --text-hint: #8a7562; + --text-light: #a89888; + --accent: #7a6050; + --accent-hover: #5c3d2e; + --brown: #5c3d2e; + --terra: #b85c3a; + --olive: #6a7a42; + --blue-grey: #6a7a8a; + --plum: #8a6a8a; + --shadow: rgba(92,61,46,0.10); + --shadow-hover: rgba(92,61,46,0.18); + --overlay: rgba(44,24,16,0.5); + } + + /* ── Dark theme ── */ + .dark { + --bg: #1a1614; + --bg-alt: #231f1c; + --bg-card: #2a2420; + --bg-input: #1a1614; + --border: #3d3530; + --border-light: #332c27; + --text: #e8ddd0; + --text-muted: #a09080; + --text-hint: #807060; + --text-light: #5a4e44; + --accent: #c4a882; + --accent-hover: #e0c8a8; + --brown: #c4a882; + --terra: #d4845a; + --olive: #8aaa5a; + --blue-grey: #8a9aaa; + --plum: #aa8aaa; + --shadow: rgba(0,0,0,0.2); + --shadow-hover: rgba(0,0,0,0.35); + --overlay: rgba(0,0,0,0.6); + } + + body { font-family: 'Lora', Georgia, serif; background: var(--bg); color: var(--text); min-height: 100vh; transition: background 0.3s, color 0.3s; } + + /* Layout */ + .app { display: flex; min-height: 100vh; } + + /* Sidebar */ + .sidebar { width: 220px; background: var(--bg-alt); border-right: 1px solid var(--border); padding: 24px 0; flex-shrink: 0; display: flex; flex-direction: column; } + .sidebar-logo { padding: 0 22px 20px; font-family: 'Playfair Display', serif; font-size: 22px; font-weight: 700; color: var(--brown); } + .sidebar-project { padding: 12px 16px; margin: 0 14px 18px; background: var(--bg-card); border-radius: 10px; font-size: 16px; color: var(--brown); border: 1px solid var(--border); } + .nav-item { padding: 12px 18px; margin: 2px 14px; border-radius: 10px; font-size: 17px; color: var(--text-muted); cursor: pointer; display: flex; align-items: center; gap: 10px; } + .nav-item:hover { background: var(--border); color: var(--text); } + .nav-item.active { background: var(--border); color: var(--text); font-weight: 500; } + .nav-icon { width: 34px; height: 34px; background: var(--border); border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; } + .nav-icon svg { width: 16px; height: 16px; stroke: var(--text-muted); fill: none; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; } + .nav-item.active .nav-icon svg { stroke: var(--text); } + .nav-icon svg.fill-icon { fill: var(--text-muted); stroke: none; } + .nav-item.active .nav-icon svg.fill-icon { fill: var(--text); } + .nav-item.active .nav-icon .frise-dot { fill: var(--text); } + .sidebar-footer { margin-top: auto; padding: 16px 22px; font-size: 13px; color: var(--text-hint); } + + /* Main */ + .main { flex: 1; display: flex; flex-direction: column; overflow: hidden; } + + /* Topbar */ + .topbar { height: 58px; background: var(--bg-card); border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; padding: 0 28px; flex-shrink: 0; } + .topbar-title { font-family: 'Playfair Display', serif; font-size: 26px; font-weight: 600; } + .topbar-right { display: flex; align-items: center; gap: 14px; } + .btn-new { padding: 10px 22px; background: var(--accent); color: var(--bg); border: none; border-radius: 8px; font-size: 16px; font-weight: 600; font-family: 'Lora', serif; cursor: pointer; } + .btn-new:hover { background: var(--accent-hover); } + .dark-toggle { width: 36px; height: 20px; background: var(--border); border-radius: 10px; position: relative; cursor: pointer; border: none; flex-shrink: 0; } + .dark-toggle::after { content: '☀'; font-size: 12px; position: absolute; top: 2px; left: 3px; width: 16px; height: 16px; background: var(--bg-card); border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: left 0.2s; } + .dark .dark-toggle::after { content: '☾'; left: 17px; } + .avatar { width: 34px; height: 34px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 600; color: #fff; } + .avatar.e { background: #5c3d2e; } + .avatar.m { background: #b85c3a; } + + /* ── Filter bar ── */ + .filter-bar { background: var(--bg-alt); border-bottom: 1px solid var(--border); padding: 10px 28px; display: flex; align-items: center; gap: 12px; flex-shrink: 0; flex-wrap: wrap; } + .filter-bar select, .filter-bar input { padding: 7px 12px; border: 1px solid var(--border); border-radius: 8px; font-size: 13px; font-family: 'Lora', serif; color: var(--text); background: var(--bg-input); outline: none; } + .filter-bar select:focus, .filter-bar input:focus { border-color: var(--accent); } + .filter-bar label { font-size: 12px; color: var(--text-hint); text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; } + .filter-group { display: flex; align-items: center; gap: 6px; } + .btn-clear-filters { padding: 6px 14px; background: transparent; border: 1px solid var(--border); border-radius: 8px; font-size: 12px; font-family: 'Lora', serif; color: var(--text-muted); cursor: pointer; } + .btn-clear-filters:hover { background: var(--border); color: var(--text); } + .card.filtered-out { display: none; } + + /* ── Frise container ── */ + .frise-wrapper { flex: 1; display: flex; flex-direction: column; overflow: hidden; } + + /* Timeline area */ + .timeline-area { flex: 1; overflow-x: auto; overflow-y: auto; padding: 20px 24px 12px; position: relative; } + + /* Acte markers row */ + .acte-markers { display: flex; gap: 0; min-width: min-content; margin-bottom: 0; position: relative; } + .acte-section { min-width: 200px; flex-shrink: 0; position: relative; } + .acte-header { text-align: center; padding-bottom: 12px; } + .acte-label { font-family: 'Playfair Display', serif; font-size: 18px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; } + .acte-section[data-acte="Prologue"] .acte-label { color: var(--blue-grey); } + .acte-section[data-acte="Acte 1"] .acte-label { color: var(--brown); } + .acte-section[data-acte="Acte 2"] .acte-label { color: var(--terra); } + .acte-section[data-acte="Acte 3"] .acte-label { color: var(--olive); } + .acte-section[data-acte="Épilogue"] .acte-label { color: var(--plum); } + + /* Timeline line */ + .timeline-line { height: 3px; background: var(--border); display: flex; min-width: min-content; border-radius: 2px; } + .timeline-segment { flex-shrink: 0; height: 3px; } + .acte-section[data-acte="Prologue"] .timeline-segment { background: var(--blue-grey); } + .acte-section[data-acte="Acte 1"] .timeline-segment { background: var(--brown); } + .acte-section[data-acte="Acte 2"] .timeline-segment { background: var(--terra); } + .acte-section[data-acte="Acte 3"] .timeline-segment { background: var(--olive); } + .acte-section[data-acte="Épilogue"] .timeline-segment { background: var(--plum); } + + /* Cards row under each acte */ + .cards-row { display: flex; gap: 14px; padding: 16px 10px 10px; min-height: 140px; align-items: flex-start; } + + /* Cards */ + .card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px; padding: 18px 20px; cursor: grab; border-top: 5px solid var(--border); width: 260px; min-width: 260px; flex-shrink: 0; box-shadow: 0 1px 4px var(--shadow); transition: box-shadow 0.15s; } + .card:hover { box-shadow: 0 4px 14px var(--shadow-hover); } + .card.sortable-ghost { opacity: 0.3; } + .card.sortable-chosen { box-shadow: 0 6px 20px var(--shadow-hover); } + .card-title { font-family: 'Playfair Display', serif; font-size: 18px; font-weight: 600; margin-bottom: 8px; color: var(--text); } + .card-summary { font-size: 15px; color: var(--text-muted); line-height: 1.5; margin-bottom: 12px; display: -webkit-box; -webkit-line-clamp: 8; -webkit-box-orient: vertical; overflow: hidden; } + .card-summary p { margin: 0 0 4px; } + .card-summary ul, .card-summary ol { margin: 0 0 4px; padding-left: 18px; } + .card-summary li { margin-bottom: 2px; } + .card-meta { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; } + .card-badge { font-size: 13px; padding: 4px 10px; border-radius: 8px; font-weight: 500; } + .card-likes { display: flex; gap: 4px; margin-left: auto; } + .like-btn { background: none; border: none; cursor: pointer; padding: 3px 6px; border-radius: 6px; font-size: 14px; color: var(--text-light); transition: color 0.15s, transform 0.15s; display: flex; align-items: center; gap: 2px; } + .like-btn:hover { color: var(--accent); transform: scale(1.15); } + .like-btn.liked { color: var(--terra); } + .like-btn .like-who { font-size: 10px; font-weight: 700; letter-spacing: 0.3px; } + .card-comment-count { font-size: 12px; color: var(--text-light); display: flex; align-items: center; gap: 3px; margin-left: 4px; } + .card-comment-count span { font-size: 13px; } + + /* Comments in modal */ + .modal-comments { border-top: 1px solid var(--border); padding-top: 16px; margin-top: 8px; } + .modal-comments h4 { font-size: 11px; text-transform: uppercase; letter-spacing: 0.6px; color: var(--text-hint); font-weight: 600; margin-bottom: 12px; } + .comment-list { display: flex; flex-direction: column; gap: 10px; margin-bottom: 14px; max-height: 240px; overflow-y: auto; } + .comment-item { background: var(--bg); border-radius: 8px; padding: 10px 14px; } + .comment-header { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; } + .comment-author { font-size: 12px; font-weight: 700; } + .comment-author.e { color: var(--brown); } + .comment-author.m { color: var(--plum); } + .comment-date { font-size: 11px; color: var(--text-light); } + .comment-delete { margin-left: auto; background: none; border: none; font-size: 13px; color: var(--text-light); cursor: pointer; padding: 0 4px; } + .comment-delete:hover { color: #8b3030; } + .comment-text { font-size: 14px; color: var(--text); line-height: 1.5; } + .comment-add { display: flex; flex-direction: column; gap: 8px; } + .comment-add textarea { width: 100%; padding: 8px 12px; border: 1px solid var(--border); border-radius: 8px; font-size: 13px; font-family: 'Lora', serif; color: var(--text); background: var(--bg-input); resize: none; min-height: 36px; max-height: 80px; outline: none; } + .comment-add textarea:focus { border-color: var(--accent); } + .comment-add-btns { display: flex; gap: 8px; justify-content: flex-end; } + .comment-send { padding: 6px 14px; background: var(--accent); color: var(--bg); border: none; border-radius: 6px; font-size: 12px; font-weight: 600; font-family: 'Lora', serif; cursor: pointer; white-space: nowrap; } + .comment-send:hover { background: var(--accent-hover); } + .comment-author-toggle { padding: 5px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; font-size: 11px; font-weight: 700; cursor: pointer; font-family: 'Lora', serif; color: var(--text-muted); } + .comment-author-toggle:hover { border-color: var(--accent); } + + /* Acte colors on card top border */ + .card[data-acte="Prologue"] { border-top-color: var(--blue-grey); } + .card[data-acte="Acte 1"] { border-top-color: var(--brown); } + .card[data-acte="Acte 2"] { border-top-color: var(--terra); } + .card[data-acte="Acte 3"] { border-top-color: var(--olive); } + .card[data-acte="Épilogue"] { border-top-color: var(--plum); } + .card[data-acte="En attente"] { border-top-color: var(--border); } + + /* Status badge colors (now applied to tags) */ + .badge-validee { background: #eef3e8; color: #5a7a3a; } + .badge-etudier { background: #f5f0e4; color: #a07850; } + .badge-abandonnee { background: var(--border); color: var(--text-hint); text-decoration: line-through; } + .badge-conflit { background: #f5e4e4; color: #8b3030; } + .dark .badge-etudier { background: #3a3020; color: #c49860; } + .dark .badge-validee { background: #2a3520; color: #8aaa6a; } + .dark .badge-conflit { background: #3a2020; color: #c06060; } + .badge-perso { background: var(--bg); color: var(--text-muted); border: 1px solid var(--border); } + .badge-tag { background: #ede8e0; color: #6b5a4a; border: 1px solid #d4c9ba; font-size: 12px; } + .dark .badge-tag { background: #3a3020; color: #c4a870; border-color: #5a4a30; } + + /* Personnages enrichis */ + .perso-card-section { margin-top: 10px; } + .perso-card-section-title { font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-hint); font-weight: 600; margin-bottom: 4px; } + .perso-card-text { font-size: 14px; color: var(--text-muted); line-height: 1.5; } + .perso-card-text p { margin: 0 0 4px; } + .perso-card-text ul, .perso-card-text ol { margin: 0 0 4px; padding-left: 18px; } + + /* Tags input */ + .tags-input-container { display: flex; flex-wrap: wrap; gap: 6px; padding: 6px 8px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-input); align-items: center; min-height: 36px; } + .tags-input-container:focus-within { border-color: var(--accent); } + .tags-input { border: none; outline: none; font-size: 13px; font-family: 'Lora', serif; color: var(--text); background: transparent; flex: 1; min-width: 80px; padding: 2px 0; } + .tag-chip { display: inline-flex; align-items: center; gap: 4px; padding: 3px 8px; background: #ede8e0; color: #6b5a4a; border-radius: 6px; font-size: 12px; font-family: 'Lora', serif; } + .dark .tag-chip { background: #3a3020; color: #c4a870; } + .tag-chip-remove { cursor: pointer; font-size: 14px; line-height: 1; opacity: 0.6; } + .tag-chip-remove:hover { opacity: 1; } + + /* ── Waiting zone (bottom) ── */ + .waiting-zone { border-top: 2px dashed var(--border); padding: 14px 24px; background: var(--bg-alt); flex-shrink: 0; } + .waiting-header { font-size: 13px; color: var(--text-hint); text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 10px; font-weight: 600; } + .waiting-cards { display: flex; gap: 12px; overflow-x: auto; min-height: 100px; padding: 4px 0; align-items: flex-start; } + .waiting-cards .card { border-style: dashed; border-top: 4px dashed var(--border); } + + /* ── Modal ── */ + .modal-overlay { display: none; position: fixed; inset: 0; background: var(--overlay); z-index: 100; align-items: center; justify-content: center; } + .modal-overlay.open { display: flex; } + .modal { background: var(--bg-card); border-radius: 16px; width: 580px; max-width: 92vw; max-height: 88vh; overflow-y: auto; box-shadow: 0 12px 40px var(--shadow-hover); } + .modal-header { padding: 22px 26px 0; display: flex; justify-content: space-between; align-items: start; } + .modal-close { background: none; border: none; font-size: 22px; color: var(--text-hint); cursor: pointer; padding: 4px 8px; } + .modal-close:hover { color: var(--text); } + .modal-body { padding: 14px 26px 26px; } + .modal-body .field { margin-bottom: 16px; } + .modal-body .field label { display: block; font-size: 11px; text-transform: uppercase; letter-spacing: 0.6px; color: var(--text-hint); font-weight: 600; margin-bottom: 5px; } + .modal-body .field input, + .modal-body .field textarea, + .modal-body .field select { width: 100%; padding: 9px 12px; border: 1px solid var(--border); border-radius: 8px; font-size: 14px; font-family: 'Lora', serif; color: var(--text); outline: none; background: var(--bg-input); } + .modal-body .field input:focus, + .modal-body .field textarea:focus, + .modal-body .field select:focus { border-color: var(--accent); } + .modal-body .field textarea { min-height: 70px; resize: vertical; line-height: 1.6; } + .modal-body .field textarea#f-resume { min-height: 140px; } + .modal-body .field-title input { font-family: 'Playfair Display', serif; font-size: 20px; font-weight: 600; border: none; background: transparent; padding: 0; } + .modal-body .field-title input:focus { border-bottom: 2px solid var(--accent); } + .modal-body .field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } + .multi-select-container { display: flex; flex-wrap: wrap; gap: 5px; padding: 8px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-input); min-height: 38px; } + .multi-select-container .ms-tag { font-size: 12px; padding: 3px 9px; border-radius: 6px; background: var(--border); color: var(--text-muted); cursor: pointer; } + .multi-select-container .ms-tag.selected { background: var(--accent); color: var(--bg); } + .multi-select-container .ms-add { font-size: 12px; padding: 3px 9px; border-radius: 6px; background: transparent; border: 1px dashed var(--border); color: var(--text-hint); cursor: pointer; } + .multi-select-container .ms-add:hover { border-color: var(--accent); color: var(--accent); } + .modal-actions { display: flex; gap: 10px; align-items: center; margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border-light); } + .modal-actions-spacer { flex: 1; } + .btn-save { padding: 9px 22px; background: var(--accent); color: var(--bg); border: none; border-radius: 8px; font-size: 14px; font-weight: 600; font-family: 'Lora', serif; cursor: pointer; } + .btn-save:hover { background: var(--accent-hover); } + .btn-delete { padding: 9px 14px; background: transparent; color: #8b3030; border: 1px solid #e8c4c0; border-radius: 8px; font-size: 13px; font-family: 'Lora', serif; cursor: pointer; } + .btn-delete:hover { background: #fdf0ef; } + .dark .btn-delete { border-color: #5a3030; } + .dark .btn-delete:hover { background: #3a2020; } + + .loading { text-align: center; padding: 60px; color: var(--text-hint); font-style: italic; } + + /* ── Vision page ── */ + .view { display: none; } + .view.active { display: flex; flex-direction: column; flex: 1; overflow: hidden; } + + .vision-content { flex: 1; overflow-y: auto; padding: 28px 36px; } + .vision-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; } + .v-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px; padding: 24px; box-shadow: 0 1px 4px var(--shadow); } + .v-card.full { grid-column: 1 / -1; } + .v-card h3 { font-family: 'Playfair Display', serif; font-size: 14px; font-weight: 600; color: var(--text-hint); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 16px; } + + .hero-banner { grid-column: 1 / -1; border-radius: 12px; height: 400px; background-image: linear-gradient(135deg, #d4a574 0%, #c4956a 30%, #a07850 60%, #7a5c40 100%); background-size: cover; background-position: center; background-repeat: no-repeat; position: relative; overflow: hidden; display: flex; align-items: flex-end; } + .dark .hero-banner { background-image: linear-gradient(135deg, #5a4030 0%, #4a3528 30%, #3a2a20 60%, #2a1e18 100%); } + .hero-banner.has-image { background-color: #1a1614; cursor: grab; } + .hero-banner.has-image.dragging { cursor: grabbing; } + .hero-banner::after { content: ''; position: absolute; inset: 0; background: linear-gradient(transparent 30%, rgba(44,24,16,0.55)); pointer-events: none; } + .hero-overlay { position: relative; z-index: 1; padding: 24px 32px; width: 100%; pointer-events: none; } + .hero-overlay [contenteditable] { pointer-events: auto; } + .hero-controls { position: absolute; top: 10px; right: 12px; z-index: 3; display: flex; gap: 8px; align-items: center; opacity: 0; transition: opacity 0.2s; } + .hero-banner:hover .hero-controls { opacity: 1; } + .hero-btn { font-size: 12px; color: rgba(255,255,255,0.8); background: rgba(44,24,16,0.5); padding: 4px 12px; border-radius: 6px; border: none; cursor: pointer; backdrop-filter: blur(4px); } + .hero-btn:hover { background: rgba(44,24,16,0.7); color: #fff; } + .hero-overlay .three-words { font-family: 'Playfair Display', serif; font-size: 32px; font-weight: 700; color: rgba(255,255,255,0.7); letter-spacing: 2px; } + .hero-overlay .hero-sub { font-size: 14px; color: rgba(255,255,255,0.75); margin-top: 6px; font-style: italic; } + + .vision-phrase { font-family: 'Playfair Display', serif; font-size: 22px; font-weight: 400; font-style: italic; line-height: 1.7; color: var(--text); } + .vision-detail { font-size: 15px; color: var(--text-muted); line-height: 1.8; margin-top: 12px; } + + .tag-group { display: flex; flex-wrap: wrap; gap: 8px; } + .tag { padding: 8px 16px; border-radius: 20px; font-size: 14px; font-weight: 500; } + .tag.ambiance { background: #f5ede4; color: #8b5e3c; border: 1px solid #ddc9b4; } + .tag.epoque { background: #eef3e8; color: #5a7a3a; border: 1px solid #c8d8b4; } + .tag.rythme { background: #f5e8e4; color: #a0523d; border: 1px solid #ddc0b4; } + .tag.genre { background: #e8ecf3; color: #4a5a7a; border: 1px solid #b4c0d8; } + .tag.theme { background: #f0eaf5; color: #6a4a7a; border: 1px solid #d0c0e0; } + .dark .tag.ambiance { background: #3a2e20; color: #c49060; border-color: #5a4530; } + .dark .tag.epoque { background: #2a3520; color: #8aaa6a; border-color: #3a4a30; } + .dark .tag.rythme { background: #3a2520; color: #c07050; border-color: #5a3530; } + .dark .tag.genre { background: #20283a; color: #7a8aaa; border-color: #303a50; } + .dark .tag.theme { background: #2a2035; color: #a080b0; border-color: #403050; } + + .two-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; } + .list-block h4 { font-family: 'Playfair Display', serif; font-size: 15px; font-weight: 600; margin-bottom: 12px; } + .list-block h4.yes, .v-card h3.yes { color: #5a7a3a; } + .list-block h4.no, .v-card h3.no { color: #a0523d; } + .dark .list-block h4.yes, .dark .v-card h3.yes { color: #8aaa6a; } + .dark .list-block h4.no, .dark .v-card h3.no { color: #c07050; } + .list-item { padding: 10px 0; font-size: 15px; color: var(--text); display: flex; align-items: flex-start; gap: 8px; } + .list-item .bullet { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; margin-top: 8px; } + .list-item.yes .bullet { background: #5a7a3a; } + .list-item.no .bullet { background: #a0523d; } + .dark .list-item.yes .bullet { background: #8aaa6a; } + .dark .list-item.no .bullet { background: #c07050; } + .list-item-title { font-weight: 600; } + .list-item-desc { font-size: 14px; color: var(--text-muted); line-height: 1.6; margin-top: 4px; } + .list-item.expanded { background: var(--bg-alt); border-radius: 8px; padding: 12px; border-left: 3px solid; margin: 4px 0; } + .list-item.expanded.yes { border-left-color: #5a7a3a; } + .list-item.expanded.no { border-left-color: #a0523d; } + .dark .list-item.expanded.yes { border-left-color: #8aaa6a; } + .dark .list-item.expanded.no { border-left-color: #c07050; } + .list-item-expand { font-size: 12px; color: var(--text-hint); cursor: pointer; margin-left: 8px; white-space: nowrap; } + .list-item-expand:hover { color: var(--accent); } + + .mood-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 18px; } + .mood-card { position: relative; border-radius: 10px; overflow: hidden; background: var(--bg-alt); border: 1px solid var(--border); } + .mood-card-img { width: 100%; aspect-ratio: 1; background-size: cover; background-position: center; background-repeat: no-repeat; cursor: pointer; background-color: var(--bg-alt); display: flex; align-items: center; justify-content: center; user-select: none; } + .mood-card-img.has-image { cursor: grab; } + .mood-card-img.has-image.dragging { cursor: grabbing; } + .mood-card-img.empty { border: 1px dashed var(--border); border-radius: 10px 10px 0 0; } + .mood-card-img.empty::after { content: '+'; font-size: 28px; color: var(--text-hint); } + .mood-card-caption { padding: 8px 10px; font-size: 13px; color: var(--text); min-height: 32px; } + .mood-card-remove { position: absolute; top: 4px; right: 4px; display: none; background: rgba(139,48,48,0.8); color: #fff; border: none; border-radius: 50%; width: 20px; height: 20px; font-size: 12px; cursor: pointer; align-items: center; justify-content: center; z-index: 2; } + .mood-card:hover .mood-card-remove { display: flex; } + .mood-card-handle { position: absolute; top: 4px; left: 4px; display: none; background: rgba(0,0,0,0.5); color: #fff; border: none; border-radius: 4px; width: 22px; height: 22px; font-size: 14px; cursor: grab; align-items: center; justify-content: center; z-index: 2; user-select: none; } + .mood-card:hover .mood-card-handle { display: flex; } + .mood-card-handle:active { cursor: grabbing; } + .mood-card.drag-over { outline: 2px solid var(--accent); outline-offset: -2px; } + .mood-card.dragging-card { opacity: 0.4; } + .mood-add { border: 1px dashed var(--border); background: transparent; display: flex; flex-direction: column; align-items: center; justify-content: center; cursor: pointer; min-height: 120px; border-radius: 10px; } + .mood-add:hover { background: var(--bg-alt); } + .mood-add-icon { font-size: 24px; color: var(--text-hint); } + + .vision-quote { font-family: 'Playfair Display', serif; font-size: 17px; font-style: italic; color: var(--text-muted); line-height: 1.7; border-left: 3px solid var(--accent); padding-left: 16px; margin: 12px 0; } + + /* ── Editable Vision ── */ + [contenteditable] { outline: none; border-radius: 6px; transition: background 0.15s, box-shadow 0.15s; cursor: text; } + [contenteditable]:hover { background: rgba(138,112,96,0.06); } + [contenteditable]:focus { background: rgba(138,112,96,0.08); box-shadow: 0 0 0 2px var(--accent); } + .dark [contenteditable]:hover { background: rgba(196,168,130,0.08); } + .dark [contenteditable]:focus { background: rgba(196,168,130,0.1); } + [contenteditable].placeholder { color: var(--text-light); font-style: italic; } + .vision-saving { position: fixed; bottom: 20px; right: 20px; background: var(--accent); color: var(--bg); padding: 8px 18px; border-radius: 8px; font-size: 13px; font-family: 'Lora', serif; opacity: 0; transition: opacity 0.3s; z-index: 200; pointer-events: none; } + .vision-saving.show { opacity: 1; } + + .tag-remove { display: none; margin-left: 4px; cursor: pointer; font-size: 11px; opacity: 0.5; } + .tag:hover .tag-remove { display: inline; } + .tag-remove:hover { opacity: 1; } + .tag-add { padding: 8px 16px; border-radius: 20px; font-size: 14px; border: 1px dashed var(--border); background: transparent; color: var(--text-hint); cursor: pointer; font-family: 'Lora', serif; } + .tag-add:hover { background: var(--bg-alt); color: var(--text); } + + .list-item-desc-preview { font-size: 14px; color: var(--text-muted); line-height: 1.6; margin-top: 4px; } + .list-item-desc-preview ul, .list-item-desc-preview ol { margin: 4px 0 4px 20px; } + .list-item-desc-preview li { margin: 2px 0; } + .list-item-desc-preview p { margin: 4px 0; } + .list-item-desc-preview p:first-child { margin-top: 0; } + .list-item-desc-preview p:last-child { margin-bottom: 0; } + + .list-item-text { flex: 1; } + .list-item-remove { visibility: hidden; cursor: pointer; color: var(--text-hint); font-size: 14px; margin-left: 8px; flex-shrink: 0; } + .list-item:hover .list-item-remove { visibility: visible; } + .list-item-remove:hover { color: #8b3030; } + .list-add { padding: 6px 0; font-size: 14px; color: var(--text-hint); cursor: pointer; display: flex; align-items: center; gap: 6px; } + .list-add:hover { color: var(--accent); } + + + /* ── Vue Rédaction ── */ + .redaction-filters { background: var(--bg-alt); border-bottom: 1px solid var(--border); padding: 8px 28px; display: flex; align-items: center; gap: 12px; flex-shrink: 0; } + .redaction-filters select, .redaction-filters input { padding: 6px 10px; border: 1px solid var(--border); border-radius: 8px; font-size: 13px; font-family: 'Lora', serif; color: var(--text); background: var(--bg-input); outline: none; } + .redaction-filters label { font-size: 12px; color: var(--text-hint); text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; } + .redaction-layout { display: flex; height: calc(100vh - 100px); } + .binder { width: 260px; flex-shrink: 0; background: var(--bg-alt); border-right: 1px solid var(--border); overflow-y: auto; padding: 16px 0; display: flex; flex-direction: column; } + .binder-acte { padding: 0 12px; margin-bottom: 4px; } + .binder-acte-title { font-family: 'Playfair Display', serif; font-size: 13px; font-weight: 700; color: var(--text-hint); text-transform: uppercase; letter-spacing: 0.5px; padding: 10px 12px 6px; cursor: pointer; user-select: none; } + .binder-acte-title:hover { color: var(--text); } + .binder-scene { padding: 8px 12px 8px 24px; font-size: 14px; color: var(--text-muted); cursor: pointer; border-radius: 6px; margin: 1px 8px; display: flex; align-items: center; gap: 8px; line-height: 1.3; } + .binder-scene:hover { background: var(--border); color: var(--text); } + .binder-scene.active { background: var(--accent); color: #fff; } + .dark .binder-scene.active { color: var(--bg); } + .binder-scene .scene-status { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; } + .binder-scene .scene-words { font-size: 11px; color: var(--text-light); margin-left: auto; white-space: nowrap; } + .binder-scene.active .scene-words { color: rgba(255,255,255,0.6); } + + .editor-pane { flex: 1; display: flex; flex-direction: column; overflow: hidden; } + .editor-header { padding: 20px 32px 12px; border-bottom: 1px solid var(--border-light); display: flex; align-items: flex-start; gap: 12px; } + .editor-scene-title { font-family: 'Playfair Display', serif; font-size: 24px; font-weight: 600; color: var(--text); } + .editor-scene-meta { font-size: 13px; color: var(--text-hint); margin-top: 4px; display: flex; gap: 16px; } + .editor-body { flex: 1; overflow-y: auto; padding: 24px 32px 80px; max-width: 720px; } + .editor-body .tiptap { outline: none; font-family: 'Lora', Georgia, serif; font-size: 16px; line-height: 1.8; color: var(--text); min-height: 300px; } + .editor-body .tiptap p { margin-bottom: 12px; } + .editor-body .tiptap h2 { font-family: 'Playfair Display', serif; font-size: 20px; font-weight: 600; margin: 24px 0 12px; } + .editor-body .tiptap h3 { font-family: 'Playfair Display', serif; font-size: 17px; font-weight: 600; margin: 20px 0 8px; } + .editor-body .tiptap strong { font-weight: 600; } + .editor-body .tiptap em { font-style: italic; } + .editor-body .tiptap ul, .editor-body .tiptap ol { margin: 8px 0 8px 24px; } + .editor-body .tiptap li { margin-bottom: 4px; } + .editor-body .tiptap hr { border: none; border-top: 1px solid var(--border); margin: 24px 0; } + .editor-body .tiptap blockquote { border-left: 3px solid var(--accent); padding-left: 16px; color: var(--text-muted); font-style: italic; margin: 16px 0; } + /* Panneau fiche (intention) — sticky en haut de l'éditeur */ + .editor-fiche { background: var(--bg-alt); border-bottom: 1px solid var(--border); font-size: 14px; color: var(--text-muted); line-height: 1.5; } + .editor-fiche-toggle { display: flex; align-items: center; gap: 8px; padding: 8px 32px; cursor: pointer; user-select: none; } + .editor-fiche-toggle:hover { background: var(--border-light); } + .editor-fiche-toggle-icon { font-size: 12px; transition: transform 0.2s; color: var(--text-hint); } + .editor-fiche-toggle-icon.open { transform: rotate(90deg); } + .editor-fiche-toggle-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.6px; color: var(--text-hint); font-weight: 600; } + .editor-fiche-content { padding: 0 32px 14px; display: none; } + .editor-fiche-content.open { display: block; } + .editor-fiche-field { margin-bottom: 10px; } + .editor-fiche-field-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-hint); font-weight: 600; margin-bottom: 3px; } + .editor-fiche-field-value { color: var(--text); } + .editor-fiche-field-value p { margin: 0 0 4px; } + .editor-fiche-field-value ul, .editor-fiche-field-value ol { margin: 0 0 4px; padding-left: 18px; } + .editor-fiche-tags { display: flex; gap: 4px; flex-wrap: wrap; } + .editor-fiche-persos { display: flex; gap: 6px; flex-wrap: wrap; } + + .editor-toolbar { display: flex; gap: 2px; padding: 8px 32px; border-bottom: 1px solid var(--border-light); background: var(--bg); } + .editor-toolbar button { background: none; border: none; padding: 6px 10px; border-radius: 4px; cursor: pointer; font-size: 14px; color: var(--text-muted); font-family: 'Lora', serif; } + .editor-toolbar button:hover { background: var(--bg-alt); color: var(--text); } + .editor-toolbar button.is-active { background: var(--border); color: var(--text); font-weight: 600; } + .editor-empty { flex: 1; display: flex; align-items: center; justify-content: center; color: var(--text-light); font-size: 16px; font-style: italic; } + .editor-saving { position: fixed; bottom: 20px; right: 20px; background: var(--accent); color: var(--bg); padding: 8px 18px; border-radius: 8px; font-size: 13px; font-family: 'Lora', serif; opacity: 0; transition: opacity 0.3s; z-index: 200; pointer-events: none; } + .editor-saving.show { opacity: 1; } + .word-count { font-size: 12px; color: var(--text-hint); padding: 8px 32px; border-top: 1px solid var(--border-light); background: var(--bg); } + .binder-legend { padding: 12px 20px; border-bottom: 1px solid var(--border); } + .binder-legend-title { font-size: 11px; color: var(--text-hint); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; } + .binder-legend-item { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-muted); margin-bottom: 3px; } + .binder-legend-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; } + + /* Comments */ + .comment-mark { cursor: pointer; border-radius: 2px; padding: 1px 0; } + .comment-mark.comment-e { background: rgba(106,122,66,0.15); border-bottom: 2px solid rgba(106,122,66,0.4); } + .comment-mark.comment-m { background: rgba(184,92,58,0.15); border-bottom: 2px solid rgba(184,92,58,0.4); } + .dark .comment-mark.comment-e { background: rgba(138,170,90,0.2); border-bottom-color: rgba(138,170,90,0.4); } + .dark .comment-mark.comment-m { background: rgba(212,132,90,0.2); border-bottom-color: rgba(212,132,90,0.4); } + + /* ── Version history panel ── */ + .version-panel { position: fixed; top: 0; right: -420px; width: 400px; height: 100vh; background: var(--bg-card); border-left: 1px solid var(--border); z-index: 250; transition: right 0.3s ease; display: flex; flex-direction: column; box-shadow: -4px 0 20px var(--shadow); } + .version-panel.open { right: 0; } + .version-panel-header { padding: 20px 24px 16px; border-bottom: 1px solid var(--border-light); display: flex; justify-content: space-between; align-items: center; } + .version-panel-header h3 { font-family: 'Playfair Display', serif; font-size: 16px; font-weight: 600; } + .version-panel-list { flex: 1; overflow-y: auto; padding: 12px; } + .version-item { padding: 14px 16px; border: 1px solid var(--border-light); border-radius: 10px; margin-bottom: 10px; cursor: pointer; transition: border-color 0.2s; } + .version-item:hover { border-color: var(--accent); } + .version-item.selected { border-color: var(--accent); background: var(--bg-alt); } + .version-item-date { font-size: 12px; color: var(--text-muted); margin-bottom: 4px; } + .version-item-label { font-size: 14px; font-weight: 500; margin-bottom: 6px; } + .version-item-preview { font-size: 12px; color: var(--text-hint); line-height: 1.5; max-height: 60px; overflow: hidden; } + .version-preview-pane { padding: 16px 24px; border-top: 1px solid var(--border-light); max-height: 40vh; overflow-y: auto; } + .version-preview-pane h4 { font-family: 'Playfair Display', serif; font-size: 14px; margin-bottom: 10px; color: var(--text-muted); } + .version-preview-content { font-size: 13px; line-height: 1.6; } + .version-actions { padding: 12px 24px 20px; border-top: 1px solid var(--border-light); display: flex; gap: 10px; } + .version-actions button { flex: 1; } + .btn-history { background: none; border: 1px solid var(--border); padding: 6px 14px; border-radius: 8px; font-family: 'Lora', serif; font-size: 13px; color: var(--text-muted); cursor: pointer; transition: all 0.2s; } + .btn-history:hover { border-color: var(--accent); color: var(--text); } + + .comment-popup { position: fixed; z-index: 300; background: var(--bg-card); border: 1px solid var(--border); border-radius: 10px; padding: 14px 18px; box-shadow: 0 4px 20px var(--shadow-hover); max-width: 320px; min-width: 200px; } + .comment-popup-author { font-size: 12px; font-weight: 600; margin-bottom: 4px; } + .comment-popup-author.e { color: var(--olive); } + .comment-popup-author.m { color: var(--terra); } + .comment-popup-date { font-size: 11px; color: var(--text-light); margin-bottom: 8px; } + .comment-popup-text { font-size: 14px; color: var(--text); line-height: 1.5; } + .comment-popup-actions { margin-top: 10px; display: flex; gap: 8px; justify-content: flex-end; } + .comment-popup-actions button { font-size: 12px; padding: 4px 12px; border-radius: 6px; border: 1px solid var(--border); background: var(--bg); color: var(--text-muted); cursor: pointer; font-family: 'Lora', serif; } + .comment-popup-actions button:hover { background: var(--bg-alt); color: var(--text); } + .comment-popup-actions button.delete { border-color: #8b3030; color: #8b3030; } + .comment-popup-actions button.delete:hover { background: #8b3030; color: #fff; } + + .toolbar-sep { width: 1px; background: var(--border); margin: 4px 6px; } + + /* ── Pages Personnages & Ressources ── */ + .page-content { padding: 28px; overflow-y: auto; height: calc(100vh - 60px); } + .page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24px; } + .page-header h2 { font-family: 'Playfair Display', serif; font-size: 28px; font-weight: 600; margin: 0; } + + .perso-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 20px; } + .perso-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; cursor: pointer; transition: box-shadow 0.15s; } + .perso-card:hover { box-shadow: 0 4px 14px var(--shadow-hover); } + .perso-card-portrait { width: 100%; height: 200px; object-fit: cover; background: var(--bg-alt); } + .perso-card-body { padding: 16px 20px; } + .perso-card-name { font-family: 'Playfair Display', serif; font-size: 20px; font-weight: 600; margin-bottom: 4px; } + .perso-card-role { font-size: 14px; color: var(--accent); margin-bottom: 8px; font-weight: 500; } + .perso-card-desc { font-size: 14px; color: var(--text-muted); line-height: 1.5; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; } + .perso-card-tags { margin-top: 10px; display: flex; gap: 6px; flex-wrap: wrap; } + .perso-card-scene-count { margin-top: 10px; font-size: 12px; color: var(--text-hint); text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; } + .perso-modal-scenes { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border); } + .perso-modal-scenes-title { font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-hint); font-weight: 600; margin-bottom: 8px; } + .perso-modal-scenes-acte { font-size: 11px; color: var(--accent); font-weight: 600; margin-top: 8px; margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.3px; } + .perso-modal-scenes-list { display: flex; flex-direction: column; gap: 2px; } + .perso-modal-scenes-list a { font-size: 13px; color: var(--text); text-decoration: none; cursor: pointer; padding: 2px 0; } + .perso-modal-scenes-list a:hover { color: var(--accent); } + + .res-filters { display: flex; gap: 12px; align-items: center; margin-bottom: 20px; flex-wrap: wrap; } + .res-filters select, .res-filters input { padding: 6px 10px; border: 1px solid var(--border); border-radius: 8px; font-size: 13px; font-family: 'Lora', serif; color: var(--text); background: var(--bg-input); outline: none; } + .res-filters label { font-size: 12px; color: var(--text-hint); text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; } + .res-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 16px; } + .res-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; cursor: pointer; transition: box-shadow 0.15s; } + .res-card:hover { box-shadow: 0 4px 14px var(--shadow-hover); } + .res-card-img { width: 100%; height: 160px; object-fit: cover; background: var(--bg-alt); } + .res-card-body { padding: 14px 16px; } + .res-card-title { font-family: 'Playfair Display', serif; font-size: 16px; font-weight: 600; margin-bottom: 4px; } + .res-card-cat { font-size: 12px; color: var(--accent); font-weight: 500; margin-bottom: 6px; } + .res-card-desc { font-size: 13px; color: var(--text-muted); line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; } + .res-card-tags { margin-top: 8px; display: flex; gap: 4px; flex-wrap: wrap; } + .res-card-link { margin-top: 8px; font-size: 12px; display: flex; align-items: center; gap: 4px; } + .res-card-link a { color: var(--accent); text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .res-card-link a:hover { text-decoration: underline; } + .res-card-link svg { width: 12px; height: 12px; flex-shrink: 0; stroke: var(--accent); fill: none; stroke-width: 2; } + .res-modal-link { margin-top: 4px; } + .res-modal-link a { color: var(--accent); text-decoration: none; font-size: 14px; word-break: break-all; } + .res-modal-link a:hover { text-decoration: underline; } + + /* Responsive */ + @media (max-width: 768px) { + .sidebar { display: none; } + .modal { width: 100%; max-width: 100%; border-radius: 12px 12px 0 0; } + .card { width: 150px; min-width: 150px; } + .vision-grid { grid-template-columns: 1fr; } + .two-cols { grid-template-columns: 1fr; } + .binder { width: 200px; } + .editor-body { padding: 16px; } + }