app
Hero Studio
Public Made by Adomby adom
The hero-image lab with a human gate: `hero-studio pose <owner/slug>` opens the studio in front of the human, posed for their package, and blocks until they click Generate, producing the provenance-stamped 2000x1250 PNG the registry's hero gate requires. The image carries the title and artwork; the exported image also bakes the type pill (top right) and full wiki slug (bottom right); the page shows the hero bare, and only the homepage marquee overlays its own dynamic info. Every hero wears its P
← Commit history
Publish 0.18.1
7 files changed
+166−52
data/pages.json+3−2@@ -62,7 +62,8 @@ "tags": [], "updated_at": "2026-07-24T15:00:09.396Z", "owner": "adom",- "slug": "hero-studio"+ "slug": "hero-studio",+ "aiThread": "hero wizard dogfood" }, { "type": "app",@@ -229,7 +230,7 @@ "score": 539, "stars": 0, "installs30d": 82,- "brief": "adom-lbr , the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad \u21c4 Altium \u21c4 EAGLE/Fusion) through one canonical adom-lbr JSON , symbol + footp",+ "brief": "adom-lbr , the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad ⇄ Altium ⇄ EAGLE/Fusion) through one canonical adom-lbr JSON , symbol + footp", "tags": [ "eda", "altium",
package.json+1−1@@ -2,7 +2,7 @@ "slug": "hero-studio", "title": "Hero Studio", "type": "app",- "version": "0.18.0",+ "version": "0.18.1", "description": "The hero-image lab with a human gate: `hero-studio pose <owner/slug>` opens the studio in front of the human, posed for their package, and blocks until they click Generate, producing the provenance-stamped 2000x1250 PNG the registry's hero gate requires. The image carries the title and artwork; the exported image also bakes the type pill (top right) and full wiki slug (bottom right); the page shows the hero bare, and only the homepage marquee overlays its own dynamic info. Adom-org pages get the house wordmark and palette automatically; every other owner brings their own accent and background colors, with placement fixed for all. The floating app window is the vitrine: it auto-loads a real screenshot from the package's wiki page (screenshots/, docs/), takes a manual import, or a full-hero import (the human-ratified re-stamp path for existing heroes). The pose gate opens in verification mode: per-item checklist (title, subtitle, vitrine, type match) and a Generate that unlocks only fully confirmed. Includes vitrine bleeds, brand tile themes, gradient angles, pixel-faithful wiki marquee and index-card previews, and a component-hero mode with a photo/raytrace/phong fallback ladder.", "license": "MIT", "tags": [
page.json+1−1@@ -2,7 +2,7 @@ "slug": "hero-studio", "title": "Hero Studio", "type": "app",- "version": "0.18.0",+ "version": "0.18.1", "description": "The hero-image lab with a human gate: `hero-studio pose <owner/slug>` opens the studio in front of the human, posed for their package, and blocks until they click Generate, producing the provenance-stamped 2000x1250 PNG the registry's hero gate requires. The image carries the title and artwork; the exported image also bakes the type pill (top right) and full wiki slug (bottom right); the page shows the hero bare, and only the homepage marquee overlays its own dynamic info. Adom-org pages get the house wordmark and palette automatically; every other owner brings their own accent and background colors, with placement fixed for all. The floating app window is the vitrine: it auto-loads a real screenshot from the package's wiki page (screenshots/, docs/), takes a manual import, or a full-hero import (the human-ratified re-stamp path for existing heroes). The pose gate opens in verification mode: per-item checklist (title, subtitle, vitrine, type match) and a Generate that unlocks only fully confirmed. Includes vitrine bleeds, brand tile themes, gradient angles, pixel-faithful wiki marquee and index-card previews, and a component-hero mode with a photo/raytrace/phong fallback ladder.", "license": "MIT", "tags": [
public/js/app.js+4@@ -213,6 +213,10 @@ function render() { if (sEl) sEl.classList.toggle('sim-fenced', simOverImport); } $('vitrine-img').src = (!vit.full && vit.src) || '';+ $('vitrine-img').onerror = () => {+ try { fetch('console', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ level: 'error', text: 'vitrine image failed to load: ' + String(vit.src).slice(0, 80) }) }); } catch {}+ handleToast({ text: 'The showcase image failed to load (see console); re-import it or pick another source.', kind: 'warn', seq: Date.now() });+ }; $('fullimport-img').src = (vit.full && vit.src) || ''; $('vitrine-full').checked = !!vit.full;
public/js/wizard.js+137−46@@ -5,6 +5,26 @@ patch, every render follows the same SSE loop the panel uses. Loads after app.js (classic scripts share top-level bindings). */ +/* ---------- snappiness: optimistic writes + rebuild-shy rendering ----------+ Through a buffering proxy the SSE echo can arrive up to a poll-tick late;+ waiting for it made every click feel like molasses (Colby, dogfood run).+ wizSend applies the patch LOCALLY first (the mock's instant feel), then+ sends; the echo is idempotent confirmation. */+function wizMerge(dst, src) {+ for (const k of Object.keys(src)) {+ if (src[k] && typeof src[k] === 'object' && !Array.isArray(src[k]) && dst[k] && typeof dst[k] === 'object') {+ wizMerge(dst[k], src[k]);+ } else {+ dst[k] = src[k];+ }+ }+}+function wizSend(patch) {+ wizMerge(state, patch);+ render();+ send(patch);+}+ /* ---------- step engine ---------- */ let wizStep = 'package'; let wizExpanded = false; // seasoned checklist expanded back to one-by-one@@ -15,6 +35,34 @@ function wp() { return pageByKey(state.page); } function wSubText() { return state.subtitle || firstSentence(wp().brief, 80); } function wSubWords() { return wSubText().split(/\s+/).filter(Boolean); } +function wizLayoutThumb(l) {+ const win = {+ 'bleed-bottom': { x:42, y:9, w:52, h:58 },+ 'bleed-right': { x:52, y:9, w:52, h:40 },+ 'bleed-top': { x:42, y:-8, w:48, h:38 },+ 'bleed-corner-br': { x:50, y:20, w:54, h:48 },+ 'bleed-corner-tr': { x:50, y:-8, w:54, h:40 },+ 'contained': { x:46, y:9, w:46, h:30 },+ 'mirror': { x:4, y:9, w:52, h:58 },+ 'showcase': { x:30, y:4, w:74, h:54 },+ }[l];+ const mirror = l === 'mirror';+ const tx = mirror ? 62 : 8;+ const scrim = l === 'showcase'+ ? '<rect x="0" y="0" width="46" height="62" fill="url(#scr' + l + ')"/>' : '';+ return '<svg viewBox="0 0 100 62" preserveAspectRatio="none">'+ + '<defs><linearGradient id="scr' + l + '" x1="0" y1="0" x2="1" y2="0">'+ + '<stop offset="0" stop-color="#05090e" stop-opacity=".95"/><stop offset="1" stop-color="#05090e" stop-opacity="0"/>'+ + '</linearGradient></defs>'+ + '<rect x="0" y="0" width="100" height="62" fill="#0d1117"/>'+ + '<rect x="' + win.x + '" y="' + win.y + '" width="' + win.w + '" height="' + win.h + '" rx="2" fill="#1c2530" stroke="#3a4552" stroke-width="1"/>'+ + scrim+ + '<rect x="' + tx + '" y="12" width="26" height="6" rx="1.5" fill="#e6edf3" opacity=".9"/>'+ + '<rect x="' + tx + '" y="22" width="22" height="3" rx="1.5" fill="#8b949e" opacity=".8"/>'+ + '<rect x="' + tx + '" y="28" width="18" height="3" rx="1.5" fill="#8b949e" opacity=".5"/>'+ + '</svg>';+}+ const WIZ_LAYOUTS = [ { v: 'bleed-bottom', label: 'Off the bottom' }, { v: 'bleed-right', label: 'Off the right' },@@ -26,16 +74,16 @@ const WIZ_LAYOUTS = [ { v: 'showcase', label: 'Backdrop' }, ]; const WIZ_THEMES = [- { v: 'midnight', label: 'Midnight' },- { v: 'pcb', label: 'Circuit board' },- { v: 'onion', label: 'Onion' },- { v: 'pill', label: 'Pill' },- { v: 'grid', label: 'Grid' },- { v: 'gradient', label: 'Gradient' },- { v: 'aurora', label: 'Aurora' },- { v: 'mesh', label: 'Mesh' },- { v: 'circuitry', label: 'Circuitry' },- { v: 'image', label: 'Your image' },+ { v: 'midnight', label: 'Midnight', sw: 'sw-midnight' },+ { v: 'pcb', label: 'Circuit board', sw: 'sw-pcb' },+ { v: 'onion', label: 'Onion arcs', sw: 'sw-onion' },+ { v: 'pill', label: 'Pill field', sw: 'sw-pill' },+ { v: 'grid', label: 'Square grid', sw: 'sw-grid' },+ { v: 'gradient', label: 'Bold gradient', sw: 'sw-grad' },+ { v: 'aurora', label: 'Aurora', sw: 'sw-aurora' },+ { v: 'mesh', label: 'Color mesh', sw: 'sw-mesh' },+ { v: 'circuitry', label: 'Generated traces', sw: 'sw-circuitry' },+ { v: 'image', label: 'Your image', sw: 'sw-image' }, ]; const WIZ_STEPS = [@@ -45,7 +93,7 @@ const WIZ_STEPS = [ summary: () => (wp().title || state.page), options: () => pages.slice(0, 24).map(pg => ({ v: pg.owner + '/' + pg.slug, label: pg.title, sub: pg.owner + '/' + pg.slug })), get: () => state.page,- set: (v) => { if (!state.verify) send({ page: v }); else handleToast({ text: 'The page is locked during a review gate.', kind: 'warn', seq: Date.now() }); },+ set: (v) => { if (!state.verify) wizSend({ page: v }); else handleToast({ text: 'The page is locked during a review gate.', kind: 'warn', seq: Date.now() }); }, }, { id: 'showcase', title: 'Showcase image',@@ -63,31 +111,31 @@ const WIZ_STEPS = [ return i >= 0 ? 'up:' + i : 'page'; }, set: (v) => {- if (v === 'mock') { send({ vitrine: { src: '', full: false } }); return; }+ if (v === 'mock') { wizSend({ vitrine: { src: '', full: false } }); return; } if (v === 'upload') { $('vitrine-file').click(); return; } if (v === 'page') { post('vitrine-from-page?page=' + encodeURIComponent(state.page)).catch(() => {}); return; }- if (v.startsWith('up:')) { send({ vitrine: { src: wizUploads[Number(v.slice(3))].data } }); }+ if (v.startsWith('up:')) { wizSend({ vitrine: { src: wizUploads[Number(v.slice(3))].data } }); } }, }, { id: 'placement', title: 'Placement', hint: 'Where the screenshot sits. Arrow keys work too.', summary: () => (WIZ_LAYOUTS.find(l => l.v === state.layout) || {}).label,- options: () => WIZ_LAYOUTS.map(l => ({ v: l.v, label: l.label, pict: l.v })),+ options: () => WIZ_LAYOUTS.map(l => ({ v: l.v, label: l.label, thumb: wizLayoutThumb(l.v) })), get: () => state.layout,- set: (v) => send({ layout: v }),+ set: (v) => wizSend({ layout: v }), }, { id: 'background', title: 'Background', hint: 'The scene behind everything.', summary: () => (WIZ_THEMES.find(t => t.v === state.theme) || {}).label,- options: () => WIZ_THEMES.map(t => ({ v: t.v, label: t.label })),+ options: () => WIZ_THEMES.map(t => ({ v: t.v, label: t.label, sw: t.sw })), get: () => state.theme, set: (v) => { if (v === 'image' && !(state.bgImage && state.bgImage.src)) { handleToast({ text: 'Pick a background file in Classic controls (bg section), or hero-studio bg --file <image>.', kind: 'info', seq: Date.now() }); }- send({ theme: v });+ wizSend({ theme: v }); }, }, {@@ -109,7 +157,7 @@ const WIZ_STEPS = [ let ht = null; hin.addEventListener('input', () => { clearTimeout(ht);- ht = setTimeout(() => { send({ headline: hin.value }); setTimeout(() => runFit(null), 300); }, 350);+ ht = setTimeout(() => { wizSend({ headline: hin.value }); setTimeout(() => runFit(null), 300); }, 350); }); const fitb = document.createElement('button'); fitb.className = 'btn small'; fitb.textContent = 'Make it as big as fits';@@ -127,7 +175,7 @@ const WIZ_STEPS = [ const n = (sin.value || firstSentence(wp().brief, 80)).split(/\s+/).filter(Boolean).length; patch.accentWords = state.accentWords.filter(i => i < n); }- send(patch);+ wizSend(patch); }, 350); }); const pick = document.createElement('div');@@ -142,7 +190,7 @@ const WIZ_STEPS = [ t.addEventListener('click', () => { const set = new Set(Array.isArray(state.accentWords) ? state.accentWords : []); set.has(i) ? set.delete(i) : set.add(i);- send({ accentWords: [...set].sort((a, b) => a - b) });+ wizSend({ accentWords: [...set].sort((a, b) => a - b) }); }); pick.appendChild(t); });@@ -169,7 +217,7 @@ const WIZ_STEPS = [ const c = document.createElement('div'); c.className = 'card wide' + (state.overlay[k] ? ' on' : ''); c.innerHTML = '<div class="card-label">' + lab + '</div><div class="card-sub">' + (state.overlay[k] ? 'Shown' : 'Hidden') + '</div>';- c.addEventListener('click', () => send({ overlay: { [k]: !state.overlay[k] } }));+ c.addEventListener('click', () => wizSend({ overlay: { [k]: !state.overlay[k] } })); wrap.appendChild(c); }); host.appendChild(wrap);@@ -195,6 +243,7 @@ function wizAllConfirmed() { function wizGoTo(id) { wizStep = id; wizExpanded = false;+ wizLastSig = ''; wizardRender(); requestAnimationFrame(positionWizRing); }@@ -283,11 +332,22 @@ function wizRenderReview(host) { vlab.className = 'wiz-lab'; vlab.textContent = 'See it as'; const seg = document.createElement('div'); seg.className = 'wseg';- [{ v: false, label: 'Exactly my PNG' }, { v: true, label: 'Homepage preview' }].forEach(o => {+ const ctxOn = document.body.classList.contains('show-ctx');+ [+ { id: 'png', label: 'Exactly my PNG' },+ { id: 'mq', label: 'Homepage preview' },+ { id: 'ctx', label: 'Everywhere it lives' },+ ].forEach(o => { const b = document.createElement('button'); b.textContent = o.label;- if (!!state.marqueePreview === o.v) b.classList.add('on');- b.addEventListener('click', () => send({ marqueePreview: o.v }));+ const active = o.id === 'ctx' ? ctxOn : (!ctxOn && !!state.marqueePreview === (o.id === 'mq'));+ if (active) b.classList.add('on');+ b.addEventListener('click', () => {+ document.body.classList.toggle('show-ctx', o.id === 'ctx');+ if (o.id !== 'ctx') wizSend({ marqueePreview: o.id === 'mq' });+ else { wizardRender(); document.getElementById('contexts').scrollIntoView({ block: 'nearest', behavior: 'smooth' }); }+ if (o.id !== 'ctx') wizardRender();+ }); seg.appendChild(b); }); const legend = document.createElement('div');@@ -309,19 +369,20 @@ function wizRenderReview(host) { ex.textContent = 'Check one by one'; ex.addEventListener('click', () => { wizExpanded = true;- send({ confirm: { title: false, subtitle: false, vitrine: false, type: false } });+ wizSend({ confirm: { title: false, subtitle: false, vitrine: false, type: false } }); }); row.append(q, ex); checks.appendChild(row); const c = state.confirm || {}; if (!(c.title && c.subtitle && c.vitrine && c.type) && !Object.values(state.flags || {}).some(Boolean)) {+ wizMerge(state, { confirm: { title: true, subtitle: true, vitrine: true, type: true } }); send({ confirm: { title: true, subtitle: true, vitrine: true, type: true } }); } } else { const allb = document.createElement('button'); allb.className = 'btn small'; allb.textContent = 'All of it looks good';- allb.addEventListener('click', () => send({+ allb.addEventListener('click', () => wizSend({ confirm: { title: true, subtitle: true, vitrine: true, type: true }, flags: { title: false, subtitle: false, vitrine: false, type: false }, }));@@ -334,11 +395,11 @@ function wizRenderReview(host) { const ok = document.createElement('button'); ok.className = 'btn small'; ok.textContent = 'Looks good';- ok.addEventListener('click', () => send({ confirm: { [c.key]: true }, flags: { [c.key]: false } }));+ ok.addEventListener('click', () => wizSend({ confirm: { [c.key]: true }, flags: { [c.key]: false } })); const fl = document.createElement('button'); fl.className = 'btn small'; fl.textContent = 'Needs fixing';- fl.addEventListener('click', () => send({ flags: { [c.key]: true }, confirm: { [c.key]: false } }));+ fl.addEventListener('click', () => wizSend({ flags: { [c.key]: true }, confirm: { [c.key]: false } })); row.append(q, ok, fl); checks.appendChild(row); });@@ -360,20 +421,20 @@ function wizRenderReview(host) { } /* ---------- confetti: the finale earns it ---------- */-function wizConfetti(fromEl) {+function wizConfetti() {+ // whole-screen rain, ABOVE the preview modal, timed to when it pops const c = document.createElement('canvas');- c.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:9999';+ c.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:100000'; c.width = innerWidth; c.height = innerHeight; document.body.appendChild(c); const g = c.getContext('2d');- const b = fromEl.getBoundingClientRect();- const ox = b.left + b.width / 2, oy = b.top + b.height / 2; const colors = ['#00b8b0', '#7a5cff', '#3fb950', '#e6edf3', '#37d6cd'];- const parts = Array.from({ length: 140 }, (_, i) => ({- x: ox, y: oy,- vx: Math.cos(i * 2.399) * (2 + (i % 7)) * (0.6 + (i % 5) / 5),- vy: Math.sin(i * 2.399) * (2 + (i % 7)) * (0.6 + (i % 5) / 5) - 6,- s: 3 + (i % 4), r: i * 0.7, vr: ((i % 9) - 4) * 0.12,+ const parts = Array.from({ length: 260 }, (_, i) => ({+ x: (i / 260) * innerWidth + ((i * 37) % 40) - 20,+ y: -20 - ((i * 53) % 160),+ vx: ((i % 11) - 5) * 0.7,+ vy: 5 + ((i * 29) % 50) / 10,+ s: 4 + (i % 5), r: i * 0.7, vr: ((i % 9) - 4) * 0.14, col: colors[i % colors.length], })); let t = 0;@@ -381,31 +442,61 @@ function wizConfetti(fromEl) { t++; g.clearRect(0, 0, c.width, c.height); for (const p of parts) {- p.x += p.vx; p.y += p.vy; p.vy += 0.22; p.vx *= 0.99; p.r += p.vr;+ p.x += p.vx; p.y += p.vy; p.vy += 0.12; p.r += p.vr; g.save(); g.translate(p.x, p.y); g.rotate(p.r);- g.fillStyle = p.col; g.globalAlpha = Math.max(0, 1 - t / 90);- g.fillRect(-p.s / 2, -p.s / 2, p.s, p.s * 0.6);+ g.fillStyle = p.col; g.globalAlpha = Math.max(0, 1.15 - t / 75);+ g.fillRect(-p.s / 2, -p.s / 2, p.s, p.s * 0.62); g.restore(); }- if (t < 95) requestAnimationFrame(tick);+ if (t < 85) requestAnimationFrame(tick); else c.remove(); })(); }-window.wizardOnGenerated = () => { const b = $('wbtn-generate'); if (b) wizConfetti(b); };+window.wizardOnGenerated = () => wizConfetti(); /* ---------- dock render ---------- */ function wizCard(o, on, onPick) { const c = document.createElement('div'); c.className = 'card' + (on ? ' on' : '') + (o.sub && o.sub.length > 24 ? ' wide' : '');- c.innerHTML = '<div class="card-label"></div>' + (o.sub ? '<div class="card-sub"></div>' : '');- c.querySelector('.card-label').textContent = o.label;- if (o.sub) c.querySelector('.card-sub').textContent = o.sub;+ if (o.thumb) {+ const t = document.createElement('div');+ t.className = 'thumb'; t.innerHTML = o.thumb;+ c.appendChild(t);+ } else if (o.sw) {+ const t = document.createElement('div');+ t.className = 'swx ' + o.sw;+ c.appendChild(t);+ }+ const l = document.createElement('div');+ l.className = 'card-label'; l.textContent = o.label;+ c.appendChild(l);+ if (o.sub) {+ const sb = document.createElement('div');+ sb.className = 'card-sub'; sb.textContent = o.sub;+ c.appendChild(sb);+ } c.addEventListener('click', onPick); return c; }+let wizLastSig = '';+function wizSignature() {+ const l = state.lint || [];+ const v = state.vitrine || {};+ return JSON.stringify([wizStep, wizExpanded, state.page, state.layout, state.theme,+ state.headline, state.subtitle, state.accentWords, state.overlay, state.confirm,+ state.flags, state.marqueePreview, state.gatesDone, wizUploads.length,+ v.full, (v.src || '').length, l.filter(x => x.level === 'fail').length,+ l.filter(x => x.level === 'warn').length, document.body.classList.contains('show-ctx')]);+} function wizardRender() { if (!document.body.classList.contains('wizard')) return; if (!state || !pages.length || !$('wdock')) return;+ const sig = wizSignature();+ if (sig === wizLastSig) return;+ const focused = document.activeElement && $('wdock').contains(document.activeElement)+ && document.activeElement.tagName === 'INPUT';+ if (focused) { wizLastSig = sig; return; }+ wizLastSig = sig; const s = wizActive(); wizRenderRail(); $('wintro-ref').textContent = state.page;@@ -477,7 +568,7 @@ $('vitrine-file').addEventListener('change', () => { r.onload = () => { wizUploads.push({ name: f.name, data: r.result }); if (wizUploads.length && f === files[files.length - 1]) {- send({ vitrine: { src: r.result } });+ wizSend({ vitrine: { src: r.result } }); } }; r.readAsDataURL(f);
public/studio.css+18@@ -357,3 +357,21 @@ body.wizard main { flex:1 1 auto; min-height:0; } .wiz-review .rv-col { flex:0 1 440px; } /* render mode exports HERO PIXELS ONLY: the wizard is stage chrome */ body[data-render] #wdock, body[data-render] #weditring { display:none !important; }+/* card previews (the visuals are the point) */+#wcards .card .thumb { width:100%; aspect-ratio:100/62; border-radius:6px; overflow:hidden; border:1px solid var(--border); }+#wcards .card .thumb svg { width:100%; height:100%; display:block; }+#wcards .card .swx { width:100%; aspect-ratio:16/10; border-radius:6px; border:1px solid var(--border); }+.sw-midnight { background:linear-gradient(155deg,#0a0e14,#0d1117 45%,#071318); }+.sw-pcb { background:linear-gradient(155deg,#03211f,#04302c); }+.sw-onion { background:repeating-radial-gradient(circle at 90% 10%, #101a2c 0 4px, #16223a 4px 6px); }+.sw-pill { background:#0c3a3c; }+.sw-grid { background:#0c3a3c url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12"><rect x="2" y="2" width="8" height="8" fill="none" stroke="%23ffffff" stroke-opacity=".35" stroke-width="1"/></svg>'); }+.sw-grad { background:linear-gradient(100deg,#00e6dc,#39b8ff 60%,#8c6bf7); }+.sw-aurora { background:radial-gradient(28px 20px at 30% 20%, rgba(0,230,220,.8), transparent), radial-gradient(34px 26px at 70% 80%, rgba(140,107,247,.7), transparent), #0a0f16; }+.sw-mesh { background:conic-gradient(from 155deg at 40% 40%, rgba(0,184,177,.5), transparent 40%), #0b1016; }+.sw-circuitry { background:repeating-linear-gradient(45deg, rgba(0,184,177,.5) 0 1.5px, transparent 1.5px 7px), #080d13; }+.sw-image { background:repeating-conic-gradient(#1c2530 0 25%, #0d1117 0 50%) 0 0/12px 12px; }+#wcards .card:has(.thumb), #wcards .card:has(.swx) { width:150px; }+/* context previews: Review-step opt-in, not standing furniture */+body.wizard #contexts { display:none; }+body.wizard.show-ctx #contexts { display:flex; }
server.js+2−2@@ -169,10 +169,10 @@ function srcHash(src) { function leanState() { const out = Object.assign({}, state); if (state.vitrine && state.vitrine.src && state.vitrine.src.startsWith('data:')) {- out.vitrine = Object.assign({}, state.vitrine, { src: '/vitrine.png?v=' + srcHash(state.vitrine.src) });+ out.vitrine = Object.assign({}, state.vitrine, { src: 'vitrine.png?v=' + srcHash(state.vitrine.src) }); } if (state.bgImage && state.bgImage.src && state.bgImage.src.startsWith('data:')) {- out.bgImage = Object.assign({}, state.bgImage, { src: '/bg.png?v=' + srcHash(state.bgImage.src) });+ out.bgImage = Object.assign({}, state.bgImage, { src: 'bg.png?v=' + srcHash(state.bgImage.src) }); } return out; }