Vision layout overhaul: moodboard + expandable list items
- Replace Inspirations with Moodboard (image cards with labels) - Tags and Moodboard on same row (two columns) - Ce qu'on veut/ne veut pas: items now support title + expandable description (click "développer" to add detail text) - Backwards compatible: old plain-string list items auto-migrate - Fix hero-meta default height to 400px - Moodboard images resized to 400px, JPEG 0.8 quality Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+148
-65
@@ -242,18 +242,32 @@
|
||||
.list-block h4.no { color: #a0523d; }
|
||||
.dark .list-block h4.yes { color: #8aaa6a; }
|
||||
.dark .list-block h4.no { color: #c07050; }
|
||||
.list-item { padding: 8px 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.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); }
|
||||
|
||||
.inspi-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 12px; }
|
||||
.inspi-card { background: var(--bg); border-radius: 10px; padding: 16px; text-align: center; border: 1px solid var(--border); }
|
||||
.inspi-icon { font-size: 28px; margin-bottom: 8px; }
|
||||
.inspi-title { font-size: 14px; font-weight: 500; color: var(--brown); }
|
||||
.inspi-type { font-size: 13px; color: var(--text-hint); margin-top: 4px; }
|
||||
.mood-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 12px; }
|
||||
.mood-card { position: relative; border-radius: 10px; overflow: hidden; aspect-ratio: 1; background: var(--bg-alt); border: 1px solid var(--border); cursor: pointer; display: flex; align-items: flex-end; background-size: cover; background-position: center; }
|
||||
.mood-card.has-image { border-color: transparent; }
|
||||
.mood-label { width: 100%; padding: 6px 10px; font-size: 12px; font-weight: 600; color: #fff; background: rgba(44,24,16,0.55); backdrop-filter: blur(4px); text-align: center; }
|
||||
.mood-card:not(.has-image) .mood-label { color: var(--text-muted); background: transparent; }
|
||||
.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-add { border: 1px dashed var(--border); background: transparent; display: flex; flex-direction: column; align-items: center; justify-content: center; cursor: pointer; aspect-ratio: 1; 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; }
|
||||
|
||||
@@ -280,9 +294,6 @@
|
||||
.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); }
|
||||
|
||||
.inspi-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; }
|
||||
.inspi-card:hover .inspi-card-remove { display: flex; }
|
||||
.inspi-card { position: relative; }
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
@@ -382,6 +393,12 @@
|
||||
</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">
|
||||
@@ -395,11 +412,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="v-card">
|
||||
<h3>Inspirations et références</h3>
|
||||
<div class="inspi-grid" id="v-inspirations"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -971,39 +983,87 @@ function removeTag(index) {
|
||||
renderTags();
|
||||
}
|
||||
|
||||
// ── Lists (on-veut / on-ne-veut-pas) ──
|
||||
// ── 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((text, i) => {
|
||||
items.forEach((item, i) => {
|
||||
const hasDesc = !!item.desc;
|
||||
const div = document.createElement('div');
|
||||
div.className = `list-item ${cssClass}`;
|
||||
div.innerHTML = `<span class="bullet"></span><span class="list-item-text" contenteditable="true">${esc(text)}</span><span class="list-item-remove" onclick="removeListItem('${section}','${containerId}','${cssClass}',${i})">×</span>`;
|
||||
const textEl = div.querySelector('.list-item-text');
|
||||
textEl.addEventListener('blur', () => {
|
||||
const newText = textEl.textContent.trim();
|
||||
if (newText && newText !== items[i]) {
|
||||
items[i] = newText;
|
||||
saveVisionSection(section, JSON.stringify(items));
|
||||
}
|
||||
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)); }
|
||||
});
|
||||
textEl.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') { e.preventDefault(); textEl.blur(); }
|
||||
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.textContent = item.desc || '';
|
||||
descEl.style.display = hasDesc ? 'block' : 'none';
|
||||
descEl.setAttribute('data-placeholder', 'Développer...');
|
||||
descEl.addEventListener('blur', () => {
|
||||
const v = descEl.textContent.trim();
|
||||
if (v !== (items[i].desc || '')) { items[i].desc = v; saveVisionSection(section, JSON.stringify(items)); }
|
||||
if (!v) { descEl.style.display = 'none'; div.classList.remove('expanded'); }
|
||||
});
|
||||
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 ? '' : 'développer →';
|
||||
expandBtn.style.display = hasDesc ? 'none' : '';
|
||||
expandBtn.onclick = () => {
|
||||
descEl.style.display = 'block';
|
||||
div.classList.add('expanded');
|
||||
expandBtn.style.display = 'none';
|
||||
descEl.focus();
|
||||
};
|
||||
|
||||
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 = '<span>+</span> Ajouter';
|
||||
addEl.innerHTML = '<span>+</span> Ajouter un élément';
|
||||
addEl.onclick = () => {
|
||||
const text = prompt('Nouvel élément :');
|
||||
if (!text) return;
|
||||
items.push(text);
|
||||
items.push({ title: text, desc: '' });
|
||||
saveVisionSection(section, JSON.stringify(items));
|
||||
renderList(section, containerId, cssClass);
|
||||
};
|
||||
@@ -1019,61 +1079,84 @@ function removeListItem(section, containerId, cssClass, index) {
|
||||
renderList(section, containerId, cssClass);
|
||||
}
|
||||
|
||||
// ── Inspirations ──
|
||||
const INSPI_ICONS = { 'Livre': '\u{1F4D6}', 'Film': '\u{1F3AC}', 'Peinture': '\u{1F3A8}', 'Musique': '\u{1F3B5}', 'Série': '\u{1F4FA}', 'Jeu': '\u{1F3AE}', 'Autre': '\u{2728}' };
|
||||
|
||||
function renderInspirations() {
|
||||
const container = document.getElementById('v-inspirations');
|
||||
const row = visionData['inspirations'];
|
||||
// ── Moodboard ──
|
||||
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 = 'inspi-card';
|
||||
card.className = 'mood-card' + (item.image ? ' has-image' : '');
|
||||
if (item.image) card.style.backgroundImage = `url(${item.image})`;
|
||||
|
||||
card.innerHTML = `
|
||||
<button class="inspi-card-remove" onclick="event.stopPropagation();removeInspiration(${i})">×</button>
|
||||
<div class="inspi-icon">${item.icon || INSPI_ICONS[item.type] || '\u{2728}'}</div>
|
||||
<div class="inspi-title" contenteditable="true">${esc(item.title)}</div>
|
||||
<div class="inspi-type">${esc(item.type)}</div>
|
||||
<button class="mood-card-remove" onclick="event.stopPropagation();removeMoodItem(${i})">×</button>
|
||||
<div class="mood-label">${esc(item.label)}</div>
|
||||
`;
|
||||
const titleEl = card.querySelector('.inspi-title');
|
||||
titleEl.addEventListener('blur', () => {
|
||||
const newTitle = titleEl.textContent.trim();
|
||||
if (newTitle && newTitle !== items[i].title) {
|
||||
items[i].title = newTitle;
|
||||
saveVisionSection('inspirations', JSON.stringify(items));
|
||||
}
|
||||
});
|
||||
titleEl.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') { e.preventDefault(); titleEl.blur(); }
|
||||
});
|
||||
card.onclick = (e) => {
|
||||
if (e.target.closest('.mood-card-remove')) return;
|
||||
moodUploadTarget = i;
|
||||
document.getElementById('mood-file-input').click();
|
||||
};
|
||||
container.appendChild(card);
|
||||
});
|
||||
|
||||
const addCard = document.createElement('div');
|
||||
addCard.className = 'inspi-card';
|
||||
addCard.style.cssText = 'border:1px dashed var(--border); background:transparent; display:flex; flex-direction:column; align-items:center; justify-content:center; cursor:pointer;';
|
||||
addCard.innerHTML = '<div style="font-size:24px; color:var(--text-hint);">+</div><div class="inspi-type">Ajouter</div>';
|
||||
addCard.className = 'mood-add';
|
||||
addCard.innerHTML = '<div class="mood-add-icon">+</div>';
|
||||
addCard.onclick = () => {
|
||||
const title = prompt('Titre de la référence :');
|
||||
if (!title) return;
|
||||
const type = prompt('Type (Livre, Film, Peinture, Musique, Série, Jeu, Autre) :', 'Livre') || 'Livre';
|
||||
items.push({ title, type, icon: INSPI_ICONS[type] || '\u{2728}' });
|
||||
saveVisionSection('inspirations', JSON.stringify(items));
|
||||
renderInspirations();
|
||||
const label = prompt('Catégorie (ex : Ambiance, Architecture, Costumes...) :');
|
||||
if (!label) return;
|
||||
items.push({ label, image: '' });
|
||||
saveVisionSection('moodboard', JSON.stringify(items));
|
||||
renderMoodboard();
|
||||
};
|
||||
container.appendChild(addCard);
|
||||
}
|
||||
|
||||
function removeInspiration(index) {
|
||||
const row = visionData['inspirations'];
|
||||
let moodUploadTarget = -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 = () => {
|
||||
const maxW = 400;
|
||||
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);
|
||||
const row = visionData['moodboard'];
|
||||
let items = [];
|
||||
try { items = JSON.parse(row?.Contenu || '[]'); } catch { items = []; }
|
||||
if (items[moodUploadTarget]) {
|
||||
items[moodUploadTarget].image = dataUrl;
|
||||
saveVisionSection('moodboard', JSON.stringify(items));
|
||||
renderMoodboard();
|
||||
}
|
||||
};
|
||||
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('inspirations', JSON.stringify(items));
|
||||
renderInspirations();
|
||||
saveVisionSection('moodboard', JSON.stringify(items));
|
||||
renderMoodboard();
|
||||
}
|
||||
|
||||
// ── Hero image with drag-to-reposition ──
|
||||
@@ -1197,7 +1280,7 @@ async function loadVision() {
|
||||
renderTags();
|
||||
renderList('on-veut', 'v-on-veut', 'yes');
|
||||
renderList('on-ne-veut-pas', 'v-on-ne-veut-pas', 'no');
|
||||
renderInspirations();
|
||||
renderMoodboard();
|
||||
} catch (e) {
|
||||
console.error('Vision load error:', e);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user