feat: reorganize Vision layout + moodboard image drag repositioning

- Phrase + Tags side by side (2 columns)
- Moodboard full width
- Synopsis full width
- On veut / On ne veut pas as separate cards (2 columns)
- Moodboard images: drag to reposition in any direction (2D)
- Double-click to replace image, single-click to upload on empty

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Etienne Delvarre
2026-05-29 14:30:07 +02:00
parent e95eedd1b8
commit 9ee2960b77
+60 -27
View File
@@ -269,10 +269,10 @@ window.dispatchEvent(new Event('tiptap-loaded'));
.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 { color: #5a7a3a; }
.list-block h4.no { color: #a0523d; }
.dark .list-block h4.yes { color: #8aaa6a; }
.dark .list-block h4.no { color: #c07050; }
.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; }
@@ -291,7 +291,9 @@ window.dispatchEvent(new Event('tiptap-loaded'));
.mood-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 16px; }
.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; cursor: pointer; background-color: var(--bg-alt); display: flex; align-items: center; justify-content: center; }
.mood-card-img { width: 100%; aspect-ratio: 1; background-size: cover; background-position: center; 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; }
@@ -473,41 +475,37 @@ window.dispatchEvent(new Event('tiptap-loaded'));
</div>
<input type="file" id="hero-file-input" accept="image/*" style="display:none;" onchange="uploadHeroImage(this)">
<div class="v-card full">
<div class="v-card">
<h3>L'histoire en une phrase</h3>
<div class="vision-phrase" id="v-phrase" contenteditable="true"></div>
<div class="vision-detail" id="v-phrase-detail" contenteditable="true"></div>
</div>
<div class="v-card">
<h3>Ambiance, époque, rythme</h3>
<div class="tag-group" id="v-tags"></div>
</div>
<div class="v-card full">
<h3>Moodboard</h3>
<div class="mood-grid" id="v-moodboard"></div>
<input type="file" id="mood-file-input" accept="image/*" style="display:none;">
</div>
<div class="v-card full">
<h3>Synopsis</h3>
<div class="vision-detail" id="v-synopsis" contenteditable="true" style="min-height:80px;"></div>
</div>
<div class="v-card">
<h3>Ambiance, époque, rythme</h3>
<div class="tag-group" id="v-tags"></div>
<h3 class="yes">Ce qu'on veut</h3>
<div id="v-on-veut"></div>
</div>
<div class="v-card">
<h3>Moodboard</h3>
<div class="mood-grid" id="v-moodboard"></div>
<input type="file" id="mood-file-input" accept="image/*" style="display:none;">
</div>
<div class="v-card full">
<h3>Ce qu'on veut / ne veut pas</h3>
<div class="two-cols">
<div class="list-block">
<h4 class="yes">On veut</h4>
<div id="v-on-veut"></div>
</div>
<div class="list-block">
<h4 class="no">On ne veut pas</h4>
<h3 class="no">Ce qu'on ne veut pas</h3>
<div id="v-on-ne-veut-pas"></div>
</div>
</div>
</div>
</div>
</div>
@@ -1222,9 +1220,42 @@ function renderMoodboard() {
card.className = 'mood-card';
const imgDiv = document.createElement('div');
imgDiv.className = 'mood-card-img' + (item.image ? '' : ' empty');
if (item.image) imgDiv.style.backgroundImage = `url(${item.image})`;
imgDiv.onclick = () => { moodUploadTarget = i; document.getElementById('mood-file-input').click(); };
const hasImg = !!item.image;
imgDiv.className = 'mood-card-img' + (hasImg ? ' has-image' : ' empty');
if (hasImg) {
imgDiv.style.backgroundImage = `url(${item.image})`;
imgDiv.style.backgroundPosition = `${item.posX ?? 50}% ${item.posY ?? 50}%`;
}
// Click to upload (only if no image or double-click)
imgDiv.ondblclick = () => { moodUploadTarget = i; document.getElementById('mood-file-input').click(); };
if (!hasImg) imgDiv.onclick = () => { moodUploadTarget = i; document.getElementById('mood-file-input').click(); };
// Drag to reposition
if (hasImg) {
let startX, startY, startPosX, startPosY;
const onMove = (e) => {
const rect = imgDiv.getBoundingClientRect();
const dx = ((e.clientX - startX) / rect.width) * -100;
const dy = ((e.clientY - startY) / rect.height) * -100;
item.posX = Math.max(0, Math.min(100, startPosX + dx));
item.posY = Math.max(0, Math.min(100, startPosY + dy));
imgDiv.style.backgroundPosition = `${item.posX}% ${item.posY}%`;
};
const onUp = () => {
imgDiv.classList.remove('dragging');
window.removeEventListener('mousemove', onMove);
window.removeEventListener('mouseup', onUp);
saveVisionSection('moodboard', JSON.stringify(items));
};
imgDiv.addEventListener('mousedown', (e) => {
if (e.button !== 0) return;
e.preventDefault();
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);
});
}
const caption = document.createElement('div');
caption.className = 'mood-card-caption';
@@ -1292,6 +1323,8 @@ document.addEventListener('DOMContentLoaded', () => {
try { items = JSON.parse(row?.Contenu || '[]'); } catch { items = []; }
if (items[moodUploadTarget]) {
items[moodUploadTarget].image = data.path;
items[moodUploadTarget].posX = 50;
items[moodUploadTarget].posY = 50;
saveVisionSection('moodboard', JSON.stringify(items));
renderMoodboard();
}