Hero image: drag to reposition + adjustable height
- Click and drag the hero image vertically to adjust framing - Height slider appears on hover below the banner - Position (Y%) and height saved to NocoDB as JSON - Backwards compatible with legacy base64-only format - Upload button moved to top-right controls overlay Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+97
-13
@@ -201,13 +201,22 @@
|
|||||||
.v-card.full { grid-column: 1 / -1; }
|
.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; }
|
.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: 280px; background: linear-gradient(135deg, #d4a574 0%, #c4956a 30%, #a07850 60%, #7a5c40 100%); position: relative; overflow: hidden; display: flex; align-items: flex-end; cursor: pointer; background-size: cover; background-position: center; }
|
.hero-banner { grid-column: 1 / -1; border-radius: 12px; height: 280px; background: linear-gradient(135deg, #d4a574 0%, #c4956a 30%, #a07850 60%, #7a5c40 100%); position: relative; overflow: hidden; display: flex; align-items: flex-end; background-size: cover; background-position: center; }
|
||||||
.dark .hero-banner { background: linear-gradient(135deg, #5a4030 0%, #4a3528 30%, #3a2a20 60%, #2a1e18 100%); }
|
.dark .hero-banner { background: linear-gradient(135deg, #5a4030 0%, #4a3528 30%, #3a2a20 60%, #2a1e18 100%); }
|
||||||
.hero-banner.has-image { background-color: #1a1614; }
|
.hero-banner.has-image { background-color: #1a1614; cursor: grab; }
|
||||||
.hero-banner::after { content: ''; position: absolute; inset: 0; background: linear-gradient(transparent 30%, rgba(44,24,16,0.55)); }
|
.hero-banner.has-image.dragging { cursor: grabbing; }
|
||||||
.hero-overlay { position: relative; z-index: 1; padding: 24px 32px; width: 100%; }
|
.hero-banner::after { content: ''; position: absolute; inset: 0; background: linear-gradient(transparent 30%, rgba(44,24,16,0.55)); pointer-events: none; }
|
||||||
.hero-upload-hint { position: absolute; top: 12px; right: 16px; z-index: 2; font-size: 12px; color: rgba(255,255,255,0.5); background: rgba(44,24,16,0.4); padding: 4px 12px; border-radius: 6px; opacity: 0; transition: opacity 0.2s; pointer-events: none; }
|
.hero-overlay { position: relative; z-index: 1; padding: 24px 32px; width: 100%; pointer-events: none; }
|
||||||
.hero-banner:hover .hero-upload-hint { opacity: 1; }
|
.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-height-control { position: absolute; bottom: -28px; left: 50%; transform: translateX(-50%); z-index: 3; display: flex; align-items: center; gap: 8px; opacity: 0; transition: opacity 0.2s; background: var(--bg-card); border: 1px solid var(--border); border-radius: 6px; padding: 4px 12px; }
|
||||||
|
.hero-banner:hover + .hero-height-control, .hero-height-control:hover { opacity: 1; }
|
||||||
|
.hero-height-control label { font-size: 11px; color: var(--text-muted); white-space: nowrap; }
|
||||||
|
.hero-height-control input[type="range"] { width: 120px; accent-color: var(--accent); }
|
||||||
|
.hero-height-control + input[type="file"] { margin-bottom: 8px; }
|
||||||
.hero-overlay .three-words { font-family: 'Playfair Display', serif; font-size: 32px; font-weight: 700; color: #fff; letter-spacing: 2px; }
|
.hero-overlay .three-words { font-family: 'Playfair Display', serif; font-size: 32px; font-weight: 700; color: #fff; letter-spacing: 2px; }
|
||||||
.hero-overlay .hero-sub { font-size: 14px; color: rgba(255,255,255,0.75); margin-top: 6px; font-style: italic; }
|
.hero-overlay .hero-sub { font-size: 14px; color: rgba(255,255,255,0.75); margin-top: 6px; font-style: italic; }
|
||||||
|
|
||||||
@@ -341,13 +350,19 @@
|
|||||||
<div class="vision-content">
|
<div class="vision-content">
|
||||||
<div class="vision-grid">
|
<div class="vision-grid">
|
||||||
|
|
||||||
<div class="hero-banner" id="hero-banner" onclick="document.getElementById('hero-file-input').click()">
|
<div class="hero-banner" id="hero-banner">
|
||||||
<span class="hero-upload-hint">Cliquer pour changer l'image</span>
|
<div class="hero-controls">
|
||||||
|
<button class="hero-btn" onclick="document.getElementById('hero-file-input').click()">Changer l'image</button>
|
||||||
|
</div>
|
||||||
<div class="hero-overlay">
|
<div class="hero-overlay">
|
||||||
<div class="three-words" id="v-trois-mots" contenteditable="true" onclick="event.stopPropagation()"></div>
|
<div class="three-words" id="v-trois-mots" contenteditable="true" onclick="event.stopPropagation()"></div>
|
||||||
<div class="hero-sub" id="v-hero-sub" contenteditable="true" onclick="event.stopPropagation()"></div>
|
<div class="hero-sub" id="v-hero-sub" contenteditable="true" onclick="event.stopPropagation()"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="hero-height-control" id="hero-height-control">
|
||||||
|
<label>Hauteur</label>
|
||||||
|
<input type="range" min="150" max="600" value="280" id="hero-height-slider" oninput="setHeroHeight(this.value)" onchange="saveHeroMeta()">
|
||||||
|
</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 full">
|
||||||
@@ -1061,7 +1076,9 @@ function removeInspiration(index) {
|
|||||||
renderInspirations();
|
renderInspirations();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Hero image ──
|
// ── Hero image with drag-to-reposition ──
|
||||||
|
let heroMeta = { posY: 50, height: 280 }; // posY in %, height in px
|
||||||
|
|
||||||
function applyHeroImage(dataUrl) {
|
function applyHeroImage(dataUrl) {
|
||||||
const banner = document.getElementById('hero-banner');
|
const banner = document.getElementById('hero-banner');
|
||||||
if (dataUrl) {
|
if (dataUrl) {
|
||||||
@@ -1073,10 +1090,74 @@ function applyHeroImage(dataUrl) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyHeroMeta() {
|
||||||
|
const banner = document.getElementById('hero-banner');
|
||||||
|
banner.style.backgroundPosition = `center ${heroMeta.posY}%`;
|
||||||
|
banner.style.height = heroMeta.height + 'px';
|
||||||
|
document.getElementById('hero-height-slider').value = heroMeta.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setHeroHeight(val) {
|
||||||
|
heroMeta.height = parseInt(val);
|
||||||
|
document.getElementById('hero-banner').style.height = val + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveHeroMeta() {
|
||||||
|
const imageData = visionData['hero-image']?.Contenu || '';
|
||||||
|
if (!imageData) return;
|
||||||
|
const payload = JSON.stringify({ image: imageData, posY: heroMeta.posY, height: heroMeta.height });
|
||||||
|
saveVisionSection('hero-image', payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadHeroImageData(raw) {
|
||||||
|
if (!raw) return;
|
||||||
|
// Try JSON format (new) or plain base64 (legacy)
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
heroMeta.posY = parsed.posY ?? 50;
|
||||||
|
heroMeta.height = parsed.height ?? 280;
|
||||||
|
applyHeroImage(parsed.image || '');
|
||||||
|
} catch {
|
||||||
|
// Legacy: raw is the base64 image directly
|
||||||
|
applyHeroImage(raw);
|
||||||
|
}
|
||||||
|
applyHeroMeta();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
function uploadHeroImage(input) {
|
function uploadHeroImage(input) {
|
||||||
const file = input.files[0];
|
const file = input.files[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
// Resize to max 1600px wide to keep base64 reasonable
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
@@ -1089,7 +1170,10 @@ function uploadHeroImage(input) {
|
|||||||
canvas.getContext('2d').drawImage(img, 0, 0, w, h);
|
canvas.getContext('2d').drawImage(img, 0, 0, w, h);
|
||||||
const dataUrl = canvas.toDataURL('image/jpeg', 0.85);
|
const dataUrl = canvas.toDataURL('image/jpeg', 0.85);
|
||||||
applyHeroImage(dataUrl);
|
applyHeroImage(dataUrl);
|
||||||
saveVisionSection('hero-image', dataUrl);
|
heroMeta.posY = 50; // Reset position on new image
|
||||||
|
applyHeroMeta();
|
||||||
|
const payload = JSON.stringify({ image: dataUrl, posY: heroMeta.posY, height: heroMeta.height });
|
||||||
|
saveVisionSection('hero-image', payload);
|
||||||
};
|
};
|
||||||
img.src = e.target.result;
|
img.src = e.target.result;
|
||||||
};
|
};
|
||||||
@@ -1118,8 +1202,8 @@ async function loadVision() {
|
|||||||
setupEditable(elId, section);
|
setupEditable(elId, section);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hero image
|
// Hero image (JSON with position/height or legacy base64)
|
||||||
applyHeroImage(visionData['hero-image']?.Contenu || '');
|
loadHeroImageData(visionData['hero-image']?.Contenu || '');
|
||||||
|
|
||||||
// Render structured sections
|
// Render structured sections
|
||||||
renderTags();
|
renderTags();
|
||||||
|
|||||||
Reference in New Issue
Block a user