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; }
|
.list-block h4.no { color: #a0523d; }
|
||||||
.dark .list-block h4.yes { color: #8aaa6a; }
|
.dark .list-block h4.yes { color: #8aaa6a; }
|
||||||
.dark .list-block h4.no { color: #c07050; }
|
.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 .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; }
|
||||||
.list-item.no .bullet { background: #a0523d; }
|
.list-item.no .bullet { background: #a0523d; }
|
||||||
.dark .list-item.yes .bullet { background: #8aaa6a; }
|
.dark .list-item.yes .bullet { background: #8aaa6a; }
|
||||||
.dark .list-item.no .bullet { background: #c07050; }
|
.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; }
|
.mood-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 12px; }
|
||||||
.inspi-card { background: var(--bg); border-radius: 10px; padding: 16px; text-align: center; border: 1px solid var(--border); }
|
.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; }
|
||||||
.inspi-icon { font-size: 28px; margin-bottom: 8px; }
|
.mood-card.has-image { border-color: transparent; }
|
||||||
.inspi-title { font-size: 14px; font-weight: 500; color: var(--brown); }
|
.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; }
|
||||||
.inspi-type { font-size: 13px; color: var(--text-hint); margin-top: 4px; }
|
.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; }
|
.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 { 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); }
|
.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 */
|
/* Responsive */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
@@ -382,6 +393,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="v-card">
|
<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>
|
<h3>Ce qu'on veut / ne veut pas</h3>
|
||||||
<div class="two-cols">
|
<div class="two-cols">
|
||||||
<div class="list-block">
|
<div class="list-block">
|
||||||
@@ -395,11 +412,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -971,39 +983,87 @@ function removeTag(index) {
|
|||||||
renderTags();
|
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) {
|
function renderList(section, containerId, cssClass) {
|
||||||
const container = document.getElementById(containerId);
|
const container = document.getElementById(containerId);
|
||||||
const row = visionData[section];
|
const row = visionData[section];
|
||||||
let items = [];
|
let items = [];
|
||||||
try { items = JSON.parse(row?.Contenu || '[]'); } catch { 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 = '';
|
container.innerHTML = '';
|
||||||
items.forEach((text, i) => {
|
items.forEach((item, i) => {
|
||||||
|
const hasDesc = !!item.desc;
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = `list-item ${cssClass}`;
|
div.className = `list-item ${cssClass}${hasDesc ? ' expanded' : ''}`;
|
||||||
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');
|
const content = document.createElement('div');
|
||||||
textEl.addEventListener('blur', () => {
|
content.className = 'list-item-text';
|
||||||
const newText = textEl.textContent.trim();
|
content.style.flex = '1';
|
||||||
if (newText && newText !== items[i]) {
|
|
||||||
items[i] = newText;
|
const titleEl = document.createElement('div');
|
||||||
saveVisionSection(section, JSON.stringify(items));
|
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 => {
|
titleEl.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); titleEl.blur(); } });
|
||||||
if (e.key === 'Enter') { e.preventDefault(); textEl.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);
|
container.appendChild(div);
|
||||||
});
|
});
|
||||||
|
|
||||||
const addEl = document.createElement('div');
|
const addEl = document.createElement('div');
|
||||||
addEl.className = 'list-add';
|
addEl.className = 'list-add';
|
||||||
addEl.innerHTML = '<span>+</span> Ajouter';
|
addEl.innerHTML = '<span>+</span> Ajouter un élément';
|
||||||
addEl.onclick = () => {
|
addEl.onclick = () => {
|
||||||
const text = prompt('Nouvel élément :');
|
const text = prompt('Nouvel élément :');
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
items.push(text);
|
items.push({ title: text, desc: '' });
|
||||||
saveVisionSection(section, JSON.stringify(items));
|
saveVisionSection(section, JSON.stringify(items));
|
||||||
renderList(section, containerId, cssClass);
|
renderList(section, containerId, cssClass);
|
||||||
};
|
};
|
||||||
@@ -1019,61 +1079,84 @@ function removeListItem(section, containerId, cssClass, index) {
|
|||||||
renderList(section, containerId, cssClass);
|
renderList(section, containerId, cssClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Inspirations ──
|
// ── Moodboard ──
|
||||||
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 renderMoodboard() {
|
||||||
|
const container = document.getElementById('v-moodboard');
|
||||||
function renderInspirations() {
|
const row = visionData['moodboard'];
|
||||||
const container = document.getElementById('v-inspirations');
|
|
||||||
const row = visionData['inspirations'];
|
|
||||||
let items = [];
|
let items = [];
|
||||||
try { items = JSON.parse(row?.Contenu || '[]'); } catch { items = []; }
|
try { items = JSON.parse(row?.Contenu || '[]'); } catch { items = []; }
|
||||||
|
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
items.forEach((item, i) => {
|
items.forEach((item, i) => {
|
||||||
const card = document.createElement('div');
|
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 = `
|
card.innerHTML = `
|
||||||
<button class="inspi-card-remove" onclick="event.stopPropagation();removeInspiration(${i})">×</button>
|
<button class="mood-card-remove" onclick="event.stopPropagation();removeMoodItem(${i})">×</button>
|
||||||
<div class="inspi-icon">${item.icon || INSPI_ICONS[item.type] || '\u{2728}'}</div>
|
<div class="mood-label">${esc(item.label)}</div>
|
||||||
<div class="inspi-title" contenteditable="true">${esc(item.title)}</div>
|
|
||||||
<div class="inspi-type">${esc(item.type)}</div>
|
|
||||||
`;
|
`;
|
||||||
const titleEl = card.querySelector('.inspi-title');
|
card.onclick = (e) => {
|
||||||
titleEl.addEventListener('blur', () => {
|
if (e.target.closest('.mood-card-remove')) return;
|
||||||
const newTitle = titleEl.textContent.trim();
|
moodUploadTarget = i;
|
||||||
if (newTitle && newTitle !== items[i].title) {
|
document.getElementById('mood-file-input').click();
|
||||||
items[i].title = newTitle;
|
};
|
||||||
saveVisionSection('inspirations', JSON.stringify(items));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
titleEl.addEventListener('keydown', e => {
|
|
||||||
if (e.key === 'Enter') { e.preventDefault(); titleEl.blur(); }
|
|
||||||
});
|
|
||||||
container.appendChild(card);
|
container.appendChild(card);
|
||||||
});
|
});
|
||||||
|
|
||||||
const addCard = document.createElement('div');
|
const addCard = document.createElement('div');
|
||||||
addCard.className = 'inspi-card';
|
addCard.className = 'mood-add';
|
||||||
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 class="mood-add-icon">+</div>';
|
||||||
addCard.innerHTML = '<div style="font-size:24px; color:var(--text-hint);">+</div><div class="inspi-type">Ajouter</div>';
|
|
||||||
addCard.onclick = () => {
|
addCard.onclick = () => {
|
||||||
const title = prompt('Titre de la référence :');
|
const label = prompt('Catégorie (ex : Ambiance, Architecture, Costumes...) :');
|
||||||
if (!title) return;
|
if (!label) return;
|
||||||
const type = prompt('Type (Livre, Film, Peinture, Musique, Série, Jeu, Autre) :', 'Livre') || 'Livre';
|
items.push({ label, image: '' });
|
||||||
items.push({ title, type, icon: INSPI_ICONS[type] || '\u{2728}' });
|
saveVisionSection('moodboard', JSON.stringify(items));
|
||||||
saveVisionSection('inspirations', JSON.stringify(items));
|
renderMoodboard();
|
||||||
renderInspirations();
|
|
||||||
};
|
};
|
||||||
container.appendChild(addCard);
|
container.appendChild(addCard);
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeInspiration(index) {
|
let moodUploadTarget = -1;
|
||||||
const row = visionData['inspirations'];
|
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 = [];
|
let items = [];
|
||||||
try { items = JSON.parse(row?.Contenu || '[]'); } catch { return; }
|
try { items = JSON.parse(row?.Contenu || '[]'); } catch { return; }
|
||||||
items.splice(index, 1);
|
items.splice(index, 1);
|
||||||
saveVisionSection('inspirations', JSON.stringify(items));
|
saveVisionSection('moodboard', JSON.stringify(items));
|
||||||
renderInspirations();
|
renderMoodboard();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Hero image with drag-to-reposition ──
|
// ── Hero image with drag-to-reposition ──
|
||||||
@@ -1197,7 +1280,7 @@ async function loadVision() {
|
|||||||
renderTags();
|
renderTags();
|
||||||
renderList('on-veut', 'v-on-veut', 'yes');
|
renderList('on-veut', 'v-on-veut', 'yes');
|
||||||
renderList('on-ne-veut-pas', 'v-on-ne-veut-pas', 'no');
|
renderList('on-ne-veut-pas', 'v-on-ne-veut-pas', 'no');
|
||||||
renderInspirations();
|
renderMoodboard();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Vision load error:', e);
|
console.error('Vision load error:', e);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user