[object Object]

← 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 {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[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,'&quot;')+')" 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,'&quot;')+')" 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,'&quot;')+')" onclick="addTextureToBoard('+JSON.stringify(t).replace(/"/g,'&quot;')+')" 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">&larr; skip · &rarr; love · &uarr; 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">&#10005;</button>
     <button class="sw-btn sw-super" data-action="up"    aria-label="Super-love">&#9733;</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 →