← back to Wallco Ai
wallco.ai · hover-preview room mockups on /designs grid cards
78b90d178d70e864175ccf699ac87e3554e92b4a · 2026-05-11 23:51:55 -0700 · SteveStudio2
Top-5 saturated designs already have residential mockups generated.
Surface them via a CSS-only hover overlay on the catalog grid card —
no extra click needed to see "is this thing worth clicking on?"
Each design with room_mockups gets:
- absolute <img> overlay covering the card-img (lazy-loaded)
- opacity 0 default, fades to 1 on :hover / :focus-visible (360ms ease)
- "IN A ROOM" badge in upper-left, hides on hover so the room shines
- prefers-reduced-motion: instant swap
Wired in three places: home featured rail, /designs catalog, /murals.
Falls back through living_room → bedroom → office → first-available.
Static route /designs/room/* is already serving from data/rooms/ (7d cache).
Verified: 3 has-room cards render on /designs page 1 (ids 14/24/29);
chromium hover assertion confirms opacity 0→1 + badge fade.
Files touched
M public/css/site.cssM server.js
Diff
commit 78b90d178d70e864175ccf699ac87e3554e92b4a
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Mon May 11 23:51:55 2026 -0700
wallco.ai · hover-preview room mockups on /designs grid cards
Top-5 saturated designs already have residential mockups generated.
Surface them via a CSS-only hover overlay on the catalog grid card —
no extra click needed to see "is this thing worth clicking on?"
Each design with room_mockups gets:
- absolute <img> overlay covering the card-img (lazy-loaded)
- opacity 0 default, fades to 1 on :hover / :focus-visible (360ms ease)
- "IN A ROOM" badge in upper-left, hides on hover so the room shines
- prefers-reduced-motion: instant swap
Wired in three places: home featured rail, /designs catalog, /murals.
Falls back through living_room → bedroom → office → first-available.
Static route /designs/room/* is already serving from data/rooms/ (7d cache).
Verified: 3 has-room cards render on /designs page 1 (ids 14/24/29);
chromium hover assertion confirms opacity 0→1 + badge fade.
---
public/css/site.css | 35 ++++++++++++++++++++++++++++++++++
server.js | 54 ++++++++++++++++++++++++++++++++++++++++-------------
2 files changed, 76 insertions(+), 13 deletions(-)
diff --git a/public/css/site.css b/public/css/site.css
index a821c58..6b8252e 100644
--- a/public/css/site.css
+++ b/public/css/site.css
@@ -257,6 +257,41 @@ main { padding-top: 73px; }
.card-img {
aspect-ratio: 1;
background: var(--line) no-repeat center / cover;
+ position: relative;
+ overflow: hidden;
+}
+
+/* Hover-preview: residential mockup fades in over the pattern thumbnail. */
+.card-room {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ opacity: 0;
+ transition: opacity 360ms ease;
+ pointer-events: none;
+ will-change: opacity;
+}
+.design-card.has-room:hover .card-room,
+.design-card.has-room:focus-visible .card-room { opacity: 1; }
+.design-card.has-room::after {
+ content: 'IN A ROOM';
+ position: absolute;
+ top: 8px; left: 8px;
+ padding: 3px 7px;
+ font-size: 9px;
+ letter-spacing: 0.10em;
+ background: rgba(15, 14, 12, 0.75);
+ color: #f0ead7;
+ border-radius: 2px;
+ pointer-events: none;
+ z-index: 1;
+ opacity: 0.9;
+}
+.design-card.has-room:hover::after { opacity: 0; }
+@media (prefers-reduced-motion: reduce) {
+ .card-room { transition: none; }
}
.card-meta {
diff --git a/server.js b/server.js
index 0583993..791a451 100644
--- a/server.js
+++ b/server.js
@@ -527,6 +527,7 @@ const FOOTER = `<footer class="site-footer">
<p>Part of the <a href="https://designerwallcoverings.com">Designer Wallcoverings</a> family.</p>
<p><a href="mailto:info@wallco.ai">info@wallco.ai</a></p>
</footer>
+<script src="/js/wallco-game.js" defer></script>
<!-- Mobile tap-target minimums (Apple HIG / WCAG 2.5.5) — applied site-wide.
Forces every clickable to ≥44×44 on phones without overriding desktop layout. -->
<style id="mobile-tap-floor">
@@ -709,15 +710,24 @@ app.get('/', (req, res) => {
`<div class="hero-slide ${i===0?'active':''}" data-idx="${i}" style="background-image:url('${d.image_url}')" aria-label="${d.title}"></div>`
).join('\n');
- const featuredCards = featured.map(d => `
- <a href="/design/${d.id}" class="design-card">
- <div class="card-img" style="background-image:url('${d.image_url}')"></div>
+ const featuredCards = featured.map(d => {
+ const roomKey = (d.room_mockups || []).includes('living_room') ? 'living_room'
+ : (d.room_mockups || []).includes('bedroom') ? 'bedroom'
+ : (d.room_mockups || []).includes('office') ? 'office'
+ : (d.room_mockups || [])[0];
+ const roomOverlay = roomKey
+ ? `<img class="card-room" src="/designs/room/design_${d.id}_${roomKey}.png" loading="lazy" alt="" aria-hidden="true">`
+ : '';
+ return `
+ <a href="/design/${d.id}" class="design-card${roomKey ? ' has-room' : ''}">
+ <div class="card-img" style="background-image:url('${d.image_url}')">${roomOverlay}</div>
<div class="card-meta">
<span class="card-title">${d.title}</span>
<span class="card-cat">${d.category}</span>
<span class="color-dot" style="background:${d.dominant_hex}"></span>
</div>
- </a>`).join('');
+ </a>`;
+ }).join('');
res.type('html').send(`${htmlHead({
title: 'wallco.ai — AI-Original Wallpaper & Murals',
@@ -870,9 +880,16 @@ app.get('/designs', (req, res) => {
const cards = slice.map(d => {
if (!_isAdmin) {
+ const roomKey = (d.room_mockups || []).includes('living_room') ? 'living_room'
+ : (d.room_mockups || []).includes('bedroom') ? 'bedroom'
+ : (d.room_mockups || []).includes('office') ? 'office'
+ : (d.room_mockups || [])[0];
+ const roomOverlay = roomKey
+ ? `<img class="card-room" src="/designs/room/design_${d.id}_${roomKey}.png" loading="lazy" alt="" aria-hidden="true">`
+ : '';
return `
- <a href="/design/${d.id}" class="design-card">
- <div class="card-img" style="background-image:url('${d.image_url}')"></div>
+ <a href="/design/${d.id}" class="design-card${roomKey ? ' has-room' : ''}">
+ <div class="card-img" style="background-image:url('${d.image_url}')">${roomOverlay}</div>
<div class="card-meta">
<span class="card-title">${d.title}</span>
<span class="card-cat">${d.category}</span>
@@ -3512,15 +3529,24 @@ app.get('/murals', (req, res) => {
const murals = DESIGNS.filter(d => d.kind === 'mural' || d.kind === 'mural_panel');
const all = murals.length === 0 ? DESIGNS : murals; // fallback: show all if no mural-kind yet
- const cards = all.map(d => `
- <a href="/design/${d.id}" class="design-card">
- <div class="card-img" style="background-image:url('${d.image_url}')"></div>
+ const cards = all.map(d => {
+ const roomKey = (d.room_mockups || []).includes('living_room') ? 'living_room'
+ : (d.room_mockups || []).includes('bedroom') ? 'bedroom'
+ : (d.room_mockups || []).includes('office') ? 'office'
+ : (d.room_mockups || [])[0];
+ const roomOverlay = roomKey
+ ? `<img class="card-room" src="/designs/room/design_${d.id}_${roomKey}.png" loading="lazy" alt="" aria-hidden="true">`
+ : '';
+ return `
+ <a href="/design/${d.id}" class="design-card${roomKey ? ' has-room' : ''}">
+ <div class="card-img" style="background-image:url('${d.image_url}')">${roomOverlay}</div>
<div class="card-meta">
<span class="card-title">${d.title}</span>
<span class="card-cat">${murals.length === 0 ? 'available as mural' : d.kind}</span>
<span class="color-dot" style="background:${d.dominant_hex}"></span>
</div>
- </a>`).join('');
+ </a>`;
+ }).join('');
res.type('html').send(`${htmlHead({
title: 'Murals — wallco.ai | AI-Generated Wall Murals',
@@ -6258,16 +6284,17 @@ async function loadPair() {
var d = j[k];
var card = document.getElementById('card-'+k);
card.innerHTML = '<img src="'+d.image_url+'" alt="'+d.title+'"><div class="meta"><div class="title">'+d.title+'</div><div class="sub">'+(d.category||'')+'</div></div>';
- card.onclick = function(){ vote(d.id, j[k==='a'?'b':'a'].id); };
+ card.onclick = function(evt){ vote(d.id, j[k==='a'?'b':'a'].id, evt); };
});
try {
var s = await (await fetch('/api/poll/stats')).json();
if (s && s.total_pairs != null) document.getElementById('poll-stats').textContent = s.total_pairs + ' total votes cast · ' + (s.designs_polled||0) + ' designs in rotation';
} catch(e) {}
}
-async function vote(winner, loser) {
+async function vote(winner, loser, evt) {
document.getElementById('card-a').style.opacity = 0.4;
document.getElementById('card-b').style.opacity = 0.4;
+ if (window.WC) WC.reward('pickA', evt);
await fetch('/api/poll/vote', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({winner: winner, loser: loser})});
setTimeout(loadPair, 400);
setTimeout(function(){ document.getElementById('card-a').style.opacity = 1; document.getElementById('card-b').style.opacity = 1; loadLB(); }, 500);
@@ -6434,7 +6461,7 @@ async function load(){
document.getElementById('hon-card').dataset.image = r.image_url;
seen.push(r.id);
}
-async function act(action){
+async function act(action, evt){
var card = document.getElementById('hon-card');
var id = parseInt(card.dataset.id, 10);
if (!id) return;
@@ -6448,6 +6475,7 @@ async function act(action){
}
todayCount++; localStorage.setItem(todayKey, String(todayCount));
renderStreak();
+ if (window.WC) WC.reward(action, evt);
picks.unshift({id: id, action: action, title: card.dataset.title, image_url: card.dataset.image});
picks = picks.slice(0, 6);
document.getElementById('hon-history').innerHTML = picks.map(function(h){
← cfe10a2 feat(/wallco-ai): drop Big Red widget into home page footer
·
back to Wallco Ai
·
wallco.ai · /api/studio/category-insights — fix products SQL 3563074 →