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:
+61
-28
@@ -269,10 +269,10 @@ window.dispatchEvent(new Event('tiptap-loaded'));
|
|||||||
|
|
||||||
.two-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
|
.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 { font-family: 'Playfair Display', serif; font-size: 15px; font-weight: 600; margin-bottom: 12px; }
|
||||||
.list-block h4.yes { color: #5a7a3a; }
|
.list-block h4.yes, .v-card h3.yes { color: #5a7a3a; }
|
||||||
.list-block h4.no { color: #a0523d; }
|
.list-block h4.no, .v-card h3.no { color: #a0523d; }
|
||||||
.dark .list-block h4.yes { color: #8aaa6a; }
|
.dark .list-block h4.yes, .dark .v-card h3.yes { color: #8aaa6a; }
|
||||||
.dark .list-block h4.no { color: #c07050; }
|
.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 { 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 .bullet { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; margin-top: 8px; }
|
||||||
.list-item.yes .bullet { background: #5a7a3a; }
|
.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-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 { 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 { 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-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-caption { padding: 8px 10px; font-size: 13px; color: var(--text); min-height: 32px; }
|
||||||
@@ -473,40 +475,36 @@ window.dispatchEvent(new Event('tiptap-loaded'));
|
|||||||
</div>
|
</div>
|
||||||
<input type="file" id="hero-file-input" accept="image/*" style="display:none;" onchange="uploadHeroImage(this)">
|
<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>
|
<h3>L'histoire en une phrase</h3>
|
||||||
<div class="vision-phrase" id="v-phrase" contenteditable="true"></div>
|
<div class="vision-phrase" id="v-phrase" contenteditable="true"></div>
|
||||||
<div class="vision-detail" id="v-phrase-detail" contenteditable="true"></div>
|
<div class="vision-detail" id="v-phrase-detail" contenteditable="true"></div>
|
||||||
</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">
|
<div class="v-card full">
|
||||||
<h3>Synopsis</h3>
|
<h3>Synopsis</h3>
|
||||||
<div class="vision-detail" id="v-synopsis" contenteditable="true" style="min-height:80px;"></div>
|
<div class="vision-detail" id="v-synopsis" contenteditable="true" style="min-height:80px;"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="v-card">
|
<div class="v-card">
|
||||||
<h3>Ambiance, époque, rythme</h3>
|
<h3 class="yes">Ce qu'on veut</h3>
|
||||||
<div class="tag-group" id="v-tags"></div>
|
<div id="v-on-veut"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="v-card">
|
<div class="v-card">
|
||||||
<h3>Moodboard</h3>
|
<h3 class="no">Ce qu'on ne veut pas</h3>
|
||||||
<div class="mood-grid" id="v-moodboard"></div>
|
<div id="v-on-ne-veut-pas"></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>
|
|
||||||
<div id="v-on-ne-veut-pas"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -1222,9 +1220,42 @@ function renderMoodboard() {
|
|||||||
card.className = 'mood-card';
|
card.className = 'mood-card';
|
||||||
|
|
||||||
const imgDiv = document.createElement('div');
|
const imgDiv = document.createElement('div');
|
||||||
imgDiv.className = 'mood-card-img' + (item.image ? '' : ' empty');
|
const hasImg = !!item.image;
|
||||||
if (item.image) imgDiv.style.backgroundImage = `url(${item.image})`;
|
imgDiv.className = 'mood-card-img' + (hasImg ? ' has-image' : ' empty');
|
||||||
imgDiv.onclick = () => { moodUploadTarget = i; document.getElementById('mood-file-input').click(); };
|
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');
|
const caption = document.createElement('div');
|
||||||
caption.className = 'mood-card-caption';
|
caption.className = 'mood-card-caption';
|
||||||
@@ -1292,6 +1323,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
try { items = JSON.parse(row?.Contenu || '[]'); } catch { items = []; }
|
try { items = JSON.parse(row?.Contenu || '[]'); } catch { items = []; }
|
||||||
if (items[moodUploadTarget]) {
|
if (items[moodUploadTarget]) {
|
||||||
items[moodUploadTarget].image = data.path;
|
items[moodUploadTarget].image = data.path;
|
||||||
|
items[moodUploadTarget].posX = 50;
|
||||||
|
items[moodUploadTarget].posY = 50;
|
||||||
saveVisionSection('moodboard', JSON.stringify(items));
|
saveVisionSection('moodboard', JSON.stringify(items));
|
||||||
renderMoodboard();
|
renderMoodboard();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user