← back to Wallco Ai
fix(/studio): escape inline onclick string quotes properly
feb477a8c368a6e460a04a54b804c3a2c9655992 · 2026-05-11 22:44:32 -0700 · SteveStudio2
Four lines in the studio template used \' inside the OUTER template
literal — but \' is a no-op escape in a backtick template (lexer
just strips the backslash), so the rendered inline-script output
contained bare ' that closed the wrapping JS string mid-attribute:
return '<button onclick="toggleExtra('+e.name+')" ...'
^ string ends here
Result: SyntaxError at <script> compile time → every event listener
in that script block fails to attach → every button on /studio is
inert. Reported by Steve as 'none of the buttons work.'
Fix: \' → \\' so the rendered output is \' (escape sequence
that the runtime browser JS parser interprets as literal ').
Applied at toggleExtra, addSearch onmouseover/out, texture-thumb
background-url, and adoptPalette onmouseover/out.
Files touched
Diff
commit feb477a8c368a6e460a04a54b804c3a2c9655992
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Mon May 11 22:44:32 2026 -0700
fix(/studio): escape inline onclick string quotes properly
Four lines in the studio template used \' inside the OUTER template
literal — but \' is a no-op escape in a backtick template (lexer
just strips the backslash), so the rendered inline-script output
contained bare ' that closed the wrapping JS string mid-attribute:
return '<button onclick="toggleExtra('+e.name+')" ...'
^ string ends here
Result: SyntaxError at <script> compile time → every event listener
in that script block fails to attach → every button on /studio is
inert. Reported by Steve as 'none of the buttons work.'
Fix: \' → \\' so the rendered output is \' (escape sequence
that the runtime browser JS parser interprets as literal ').
Applied at toggleExtra, addSearch onmouseover/out, texture-thumb
background-url, and adoptPalette onmouseover/out.
---
server.js | 138 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 133 insertions(+), 5 deletions(-)
diff --git a/server.js b/server.js
index 3b81b70..e6528c6 100644
--- a/server.js
+++ b/server.js
@@ -404,6 +404,26 @@ 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>
+<!-- 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">
+@media (max-width: 640px) {
+ button:not(.hero-dot):not(.palette-dot):not([aria-hidden="true"]),
+ .btn-primary, .btn-outline,
+ a.proj-card, .play-card, .lb-card,
+ .material-tabs > *, .nav-panel a,
+ .filter-pill, .dots-sort, .edit-tab,
+ input[type="submit"] {
+ min-height: 44px;
+ }
+ /* Tighter line-height on those that grow */
+ .material-tabs > *, .dots-sort, .filter-pill { padding-top: 10px; padding-bottom: 10px; line-height: 1.2; }
+ /* Side-by-side leaderboard cards stay tappable */
+ .lb-card .meta { padding: 10px 10px 12px; min-height: 44px; }
+ /* Make tab triggers fat-finger friendly */
+ .edit-tab { padding: 14px 12px !important; }
+}
+</style>
<script src="/js/chat.js" defer></script>`;
const HAMBURGER_JS = `
@@ -2567,6 +2587,114 @@ ${htmlHeader('/designs')}
</script>
${_isAdmin ? `
+ <!-- Admin: isolate colors (palette + SW + coordinating) -->
+ <div id="iso-block" style="margin-top:28px;padding:18px;border:1px solid var(--line);border-radius:10px;background:rgba(0,0,0,.02)">
+ <div style="display:flex;justify-content:space-between;align-items:baseline;gap:10px;margin-bottom:10px;flex-wrap:wrap">
+ <h3 style="font-family:var(--serif);font-weight:300;font-size:18px;margin:0">Color isolate</h3>
+ <span style="font-size:11px;color:var(--ink-faint);letter-spacing:.04em">Admin-only · palette + Sherwin-Williams + coordinating</span>
+ </div>
+ <div id="iso-palette-detail" style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px"><span style="color:var(--ink-faint);font-size:12px">loading palette…</span></div>
+ <div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
+ <button id="iso-isolate-btn" type="button" style="padding:9px 16px;background:#1a1816;color:#d2b15c;border:1px solid #d2b15c;border-radius:3px;cursor:pointer;font:13px/1 var(--sans);font-weight:600;letter-spacing:.06em;text-transform:uppercase">Isolate this design</button>
+ <div id="iso-mode-detail" style="display:none;gap:0;border:1px solid var(--line);border-radius:3px;overflow:hidden">
+ <button type="button" data-mode="wallpaper" style="padding:8px 14px;background:#1a1816;color:#fff;border:none;cursor:pointer;font:11px/1 var(--sans);letter-spacing:.06em;text-transform:uppercase">Wallpaper</button>
+ <button type="button" data-mode="paint" style="padding:8px 14px;background:transparent;color:var(--ink-soft);border:none;cursor:pointer;font:11px/1 var(--sans);letter-spacing:.06em;text-transform:uppercase">Paint</button>
+ </div>
+ </div>
+ <div id="iso-section-wp-detail" style="display:none;margin-top:14px">
+ <div style="font-size:11px;color:var(--ink-faint);letter-spacing:.08em;text-transform:uppercase;margin-bottom:8px">Coordinating wallpapers</div>
+ <div id="iso-grid-wp-detail" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:8px"></div>
+ </div>
+ <div id="iso-section-pt-detail" style="display:none;margin-top:14px">
+ <div style="font-size:11px;color:var(--ink-faint);letter-spacing:.08em;text-transform:uppercase;margin-bottom:8px">Coordinating paint colors</div>
+ <div id="iso-grid-pt-detail" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px"></div>
+ </div>
+ </div>
+ <script>
+ (function(){
+ var ID = ${design.id};
+ var DATA = null;
+ function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g, function(c){return {'&':'&','<':'<','>':'>','"':'"'}[c];}); }
+ var palEl = document.getElementById('iso-palette-detail');
+ var btn = document.getElementById('iso-isolate-btn');
+ var mode = document.getElementById('iso-mode-detail');
+ var wpSec = document.getElementById('iso-section-wp-detail');
+ var ptSec = document.getElementById('iso-section-pt-detail');
+ var wpG = document.getElementById('iso-grid-wp-detail');
+ var ptG = document.getElementById('iso-grid-pt-detail');
+ function renderPal(palette){
+ if (!palette || !palette.length) { palEl.innerHTML = '<span style="color:var(--ink-faint);font-size:12px">no palette</span>'; return; }
+ palEl.innerHTML = palette.map(function(p){
+ var sw = p.sw;
+ return '<div style="display:flex;flex-direction:column;align-items:center;gap:4px;min-width:80px;padding:6px;background:#fff;border:1px solid var(--line);border-radius:4px">' +
+ '<div style="width:48px;height:48px;border-radius:50%;background:'+esc(p.hex)+';border:1px solid rgba(0,0,0,.08)"></div>' +
+ '<div style="font-family:ui-monospace,monospace;font-size:10px;color:var(--ink)">'+esc(p.hex)+'</div>' +
+ (sw ? '<div style="font-size:10px;color:#a67b1f">'+esc(sw.code)+'</div>'+
+ '<div style="font-size:10px;color:var(--ink-faint);text-align:center;line-height:1.2;max-width:90px">'+esc(sw.name)+'</div>'
+ : '<div style="font-size:10px;color:var(--ink-faint);font-style:italic">—</div>') +
+ '</div>';
+ }).join('');
+ }
+ function renderWP(designs){
+ if (!designs || !designs.length) { wpG.innerHTML = '<div style="grid-column:1/-1;color:var(--ink-faint);font-size:12px">no coordinating wallpapers yet</div>'; return; }
+ wpG.innerHTML = designs.map(function(d){
+ var url = (d.image_url||'').replace(/'/g,'');
+ return '<a href="/design/'+d.id+'" style="display:block;aspect-ratio:1/1;background:#0d0c0a center/cover url('+url+');border-radius:3px;border:1px solid var(--line);position:relative">' +
+ '<span style="position:absolute;bottom:2px;right:3px;font:9px/1 ui-monospace,monospace;color:#fff;background:rgba(0,0,0,.6);padding:1px 4px;border-radius:2px">ΔE '+(d.deltaE!=null?d.deltaE.toFixed(1):'?')+'</span></a>';
+ }).join('');
+ }
+ function renderPT(coord){
+ if (!coord || !Object.keys(coord).length) { ptG.innerHTML = '<div style="grid-column:1/-1;color:var(--ink-faint);font-size:12px">no coordinating colors</div>'; return; }
+ var labelMap = {complement:'Complement',analogousLeft:'Analogous −30°',analogousRight:'Analogous +30°',triadicA:'Triadic +120°',triadicB:'Triadic +240°',splitComplementA:'Split-comp +150°',splitComplementB:'Split-comp +210°'};
+ ptG.innerHTML = Object.keys(coord).map(function(k){
+ var v = coord[k], sw = v.sw;
+ return '<div style="display:flex;flex-direction:column;gap:5px;padding:8px;background:#fff;border:1px solid var(--line);border-radius:4px">' +
+ '<div style="width:100%;aspect-ratio:1/1;background:'+esc(v.hex)+';border-radius:3px;border:1px solid rgba(0,0,0,.08)"></div>' +
+ '<div style="font-size:9px;color:var(--ink-faint);text-transform:uppercase;letter-spacing:.08em">'+esc(labelMap[k]||k)+'</div>' +
+ '<div style="font-family:ui-monospace,monospace;font-size:11px;color:var(--ink)">'+esc(v.hex)+'</div>' +
+ (sw ? '<div style="font-size:10px;color:#a67b1f">'+esc(sw.code)+'</div>'+
+ '<div style="font-size:10px;color:var(--ink-soft);line-height:1.2">'+esc(sw.name)+'</div>'
+ : '<div style="font-size:10px;color:var(--ink-faint);font-style:italic">SW DB not loaded</div>') +
+ '<button type="button" data-copy="'+esc(v.hex)+'" style="margin-top:4px;padding:4px 6px;background:#fff;border:1px solid var(--line);color:var(--ink-soft);font:10px/1 var(--sans);cursor:pointer;border-radius:2px">copy hex</button>' +
+ '</div>';
+ }).join('');
+ ptG.querySelectorAll('button[data-copy]').forEach(function(b){
+ b.addEventListener('click', function(){ navigator.clipboard.writeText(b.dataset.copy).then(function(){ b.textContent='copied!'; setTimeout(function(){b.textContent='copy hex';},1200); }).catch(function(){ b.textContent='copy failed';}); });
+ });
+ }
+ function load(){
+ fetch('/api/isolate/' + ID).then(function(r){ return r.ok ? r.json() : Promise.reject(r.status); }).then(function(d){
+ DATA = d;
+ renderPal(d.palette || []);
+ }).catch(function(err){
+ palEl.innerHTML = '<span style="color:var(--ink-faint);font-size:12px">failed: '+esc(err)+'</span>';
+ });
+ }
+ btn.addEventListener('click', function(){
+ if (!DATA) return;
+ mode.style.display = 'flex';
+ wpSec.style.display = 'block';
+ ptSec.style.display = 'none';
+ btn.disabled = true; btn.style.opacity = '.55'; btn.style.cursor = 'default';
+ btn.textContent = 'Isolated · pick a mode';
+ renderWP(DATA.coordinating_designs || []);
+ renderPT(DATA.coordinating_hexes || {});
+ });
+ mode.addEventListener('click', function(e){
+ var b = e.target.closest('button[data-mode]'); if (!b) return;
+ var m = b.dataset.mode;
+ Array.from(mode.querySelectorAll('button')).forEach(function(x){
+ var on = x === b;
+ x.style.background = on ? '#1a1816' : 'transparent';
+ x.style.color = on ? '#fff' : 'var(--ink-soft)';
+ });
+ wpSec.style.display = m === 'wallpaper' ? 'block' : 'none';
+ ptSec.style.display = m === 'paint' ? 'block' : 'none';
+ });
+ load();
+ })();
+ </script>
+
<!-- Admin: rate this design (RLHF-lite for the generator) -->
<div id="rate-block" style="margin-top:28px;padding:18px;border:1px solid var(--line);border-radius:10px;background:rgba(0,0,0,.02)">
<h3 style="font-family:var(--serif);font-weight:300;font-size:18px;margin:0 0 4px">Rate this design</h3>
@@ -5285,7 +5413,7 @@ async function loadRefs(){
elements.forEach(function(e){ (byCat[e.category]=byCat[e.category]||[]).push(e); });
document.getElementById('elements-list').innerHTML = Object.keys(byCat).sort().map(function(c){
return '<div style="margin-bottom:6px"><div style="font-weight:600;font-size:11px;color:#666;text-transform:uppercase;letter-spacing:.06em;margin-bottom:3px">'+c+'</div><div style="display:flex;flex-wrap:wrap">'+byCat[c].map(function(e){
- return '<button onclick="toggleExtra(\''+e.name+'\')" style="padding:2px 7px;font-size:10px;border-radius:9px;border:1px solid #ccc;background:#fff;color:#333;cursor:pointer;margin:1px">'+e.name+'</button>';
+ return '<button onclick="toggleExtra(\\''+e.name+'\\')" style="padding:2px 7px;font-size:10px;border-radius:9px;border:1px solid #ccc;background:#fff;color:#333;cursor:pointer;margin:1px">'+e.name+'</button>';
}).join('')+'</div></div>';
}).join('');
renderExtra();
@@ -5360,7 +5488,7 @@ async function doSearch(){
var thumb = r.thumb_url || '';
var label = (r.title||'').slice(0,40);
var meta = (r.artist||r.subsource||'') + (r.dominant_hex ? ' · '+r.dominant_hex : '');
- return '<div onclick="addSearch('+JSON.stringify(r).replace(/"/g,'"')+')" style="cursor:pointer;border:1px solid #eee;border-radius:4px;padding:4px;background:#fff;transition:transform .15s" onmouseover="this.style.transform=\'scale(1.03)\'" onmouseout="this.style.transform=\'\'">'+
+ return '<div onclick="addSearch('+JSON.stringify(r).replace(/"/g,'"')+')" style="cursor:pointer;border:1px solid #eee;border-radius:4px;padding:4px;background:#fff;transition:transform .15s" onmouseover="this.style.transform=\\'scale(1.03)\\'" onmouseout="this.style.transform=\\'\\'">'+
(thumb?'<img src="'+thumb+'" style="width:100%;aspect-ratio:1;object-fit:cover;border-radius:3px" loading="lazy">':'<div style="aspect-ratio:1;background:#'+((r.dominant_hex||'#888').replace('#',''))+'"></div>')+
'<div style="font-size:10px;margin-top:3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="'+label+'">'+label+'</div>'+
'<div style="font-size:9px;color:#888;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+meta+'</div>'+
@@ -5430,7 +5558,7 @@ async function loadTextures() {
var tx = await get('/api/studio/textures');
document.getElementById('textures-grid').innerHTML = tx.map(function(t){
return '<div draggable="true" ondragstart="dragTexture(event,'+JSON.stringify(t).replace(/"/g,'"')+')" onclick="addTextureToBoard('+JSON.stringify(t).replace(/"/g,'"')+')" style="cursor:grab;text-align:center;border:1px solid #eee;border-radius:4px;padding:3px;background:#fff;font-size:9px" title="'+t.name+' — '+t.category+'">'+
- '<div style="aspect-ratio:1;background:url(\''+t.image_url+'\') center/cover, '+t.dominant_hex+';border-radius:3px"></div>'+
+ '<div style="aspect-ratio:1;background:url(\\''+t.image_url+'\\') center/cover, '+t.dominant_hex+';border-radius:3px"></div>'+
'<div style="margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+t.name+'</div>'+
'</div>';
}).join('');
@@ -5494,7 +5622,7 @@ async function loadSuggestions(){
var j = await post('/api/studio/suggest-palettes', { sid: SID });
var div = document.getElementById('palette-suggestions');
div.innerHTML = (j.palettes||[]).map(function(p,idx){
- return '<div onclick="adoptPalette('+idx+')" style="cursor:pointer;padding:10px;border:1px solid #eee;border-radius:4px;background:#fafafa" onmouseover="this.style.background=\'#fff\'" onmouseout="this.style.background=\'#fafafa\'">'+
+ return '<div onclick="adoptPalette('+idx+')" style="cursor:pointer;padding:10px;border:1px solid #eee;border-radius:4px;background:#fafafa" onmouseover="this.style.background=\\'#fff\\'" onmouseout="this.style.background=\\'#fafafa\\'">'+
'<div style="font-size:12px;font-weight:500;margin-bottom:6px">'+p.name+'</div>'+
'<div style="display:flex;gap:2px;margin-bottom:6px">'+(p.colors||[]).map(function(c){return '<div style="flex:1;height:32px;background:'+c+';border-radius:2px"></div>';}).join('')+'</div>'+
'<div style="font-size:10px;color:#666">'+p.rationale+'</div>'+
@@ -6151,7 +6279,7 @@ ${htmlHeader('')}
<h1 style="font-family:'Cormorant Garamond',serif;font-weight:300;font-size:30px;margin:0 0 4px">Swipe</h1>
<p style="color:#666;margin:0 0 14px;font-size:12px">← skip · → love · ↑ super-love · drag the card · or use the buttons.</p>
<div id="sw-streak" style="margin-bottom:10px;font-size:12px;color:#d2b15c;font-weight:500"></div>
- <div id="sw-stack" style="position:relative;height:560px;margin:0 auto;max-width:420px"></div>
+ <div id="sw-stack" style="position:relative;height:clamp(380px, 70vh, 560px);margin:0 auto;max-width:420px"></div>
<div id="sw-buttons" style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-top:14px;max-width:380px;margin-left:auto;margin-right:auto">
<button class="sw-btn sw-skip" data-action="left" aria-label="Skip">✕</button>
<button class="sw-btn sw-super" data-action="up" aria-label="Super-love">★</button>
← 24e8de3 wallco.ai · surface pre-generated room mockups on /design/:i
·
back to Wallco Ai
·
wallco.ai · multi-room mockup grid + bedroom for top-5 (tick 468964c →