[object Object]

← back to Wallco Ai

wallco.ai: width + material picker on /design/:id — 24" residential (Paper/Vinyl/Peel-Stick/Grasscloth) vs 54" commercial (Vinyl Type II only) · localStorage-persisted · spec panel reactive · 'Designed by wallco.ai' (no Replicate leak)

b8132ca3d968f1145ed1019813f4ac23f317cd2a · 2026-05-11 19:20:45 -0700 · Steve

Files touched

Diff

commit b8132ca3d968f1145ed1019813f4ac23f317cd2a
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon May 11 19:20:45 2026 -0700

    wallco.ai: width + material picker on /design/:id — 24" residential (Paper/Vinyl/Peel-Stick/Grasscloth) vs 54" commercial (Vinyl Type II only) · localStorage-persisted · spec panel reactive · 'Designed by wallco.ai' (no Replicate leak)
---
 data/moodboards.json | 17 ++++++++++-
 data/pairings.json   | 70 ++++++++++++++++++++++++++++++++++++++++++-
 server.js            | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++--
 3 files changed, 167 insertions(+), 5 deletions(-)

diff --git a/data/moodboards.json b/data/moodboards.json
index 9e26dfe..867a977 100644
--- a/data/moodboards.json
+++ b/data/moodboards.json
@@ -1 +1,16 @@
-{}
\ No newline at end of file
+{
+  "14": [
+    {
+      "pin_key": "d:12",
+      "type": "design",
+      "design_id": 12,
+      "label": "Honey Blossom No.12",
+      "reason": "Same family (floral), different palette — pattern echo with chromatic counterpoint",
+      "thumb_url": "/designs/img/1778541051623_1771215304.png",
+      "hex": "#a09279",
+      "idx": 0,
+      "pinned": false,
+      "pinned_at": "2026-05-12T02:05:08.208Z"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/data/pairings.json b/data/pairings.json
index 9e26dfe..b48d4c9 100644
--- a/data/pairings.json
+++ b/data/pairings.json
@@ -1 +1,69 @@
-{}
\ No newline at end of file
+{
+  "14": {
+    "design_id": 14,
+    "vision_description": "Floral pattern with yellow flowers on a dark background, large-scale repeating floral motif, formal, rich, and luxurious atmosphere.",
+    "generated_at": "2026-05-12T02:05:08.028Z",
+    "suggestions": [
+      {
+        "type": "design",
+        "design_id": 12,
+        "label": "Honey Blossom No.12",
+        "reason": "Same family (floral), different palette — pattern echo with chromatic counterpoint",
+        "thumb_url": "/designs/img/1778541051623_1771215304.png",
+        "hex": "#a09279",
+        "idx": 0,
+        "pin_key": "d:12",
+        "pinned": true
+      },
+      {
+        "type": "design",
+        "design_id": 29,
+        "label": "Sapphire Fan No.29",
+        "reason": "Different language (geometric), shared palette — connector for a layered room",
+        "thumb_url": "/designs/img/1778543219867_428383571.png",
+        "hex": "#0b1631",
+        "idx": 1,
+        "pin_key": "d:29",
+        "pinned": false
+      },
+      {
+        "type": "color",
+        "hex": "#806224",
+        "label": "Complement · #806224",
+        "reason": "Direct chromatic opposite — trim, hardware, or a single accent piece",
+        "idx": 2,
+        "pin_key": "c:#806224",
+        "pinned": false
+      },
+      {
+        "type": "color",
+        "hex": "#342480",
+        "label": "Analogous · #342480",
+        "reason": "Neighboring hue — supporting fabric or upholstery, harmonious not loud",
+        "idx": 3,
+        "pin_key": "c:#342480",
+        "pinned": false
+      },
+      {
+        "type": "material",
+        "label": "Gold leaf accents",
+        "reason": "Contrasts with dark background, enhances floral luxury and adds opulence through metallic sheen.",
+        "icon": "✦",
+        "vision_grounded": true,
+        "idx": 4,
+        "pin_key": "m:gold-leaf-accents",
+        "pinned": false
+      },
+      {
+        "type": "material",
+        "label": "Walnut wood paneling",
+        "reason": "Rich, warm tones complement dark base, adding texture and depth to formal, luxurious atmosphere.",
+        "icon": "✦",
+        "vision_grounded": true,
+        "idx": 5,
+        "pin_key": "m:walnut-wood-paneling",
+        "pinned": false
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/server.js b/server.js
index 3c1c6fa..97bd838 100644
--- a/server.js
+++ b/server.js
@@ -1338,17 +1338,96 @@ ${htmlHeader('/designs')}
         <span class="hex-label">${design.dominant_hex}</span>
       </div>
 
+      <!-- Width + material selector -->
+      <div class="material-picker" style="margin:18px 0 20px;padding:16px 18px;border:1px solid var(--line);border-radius:8px;background:var(--card-bg)">
+        <div style="font:11px var(--sans);text-transform:uppercase;letter-spacing:.08em;color:var(--ink-faint);margin-bottom:10px">Width</div>
+        <div class="width-tabs" id="width-tabs" style="display:flex;gap:8px;margin-bottom:14px">
+          <button type="button" class="w-tab is-on" data-w="24" style="flex:1;padding:10px 14px;border:1px solid var(--accent);background:var(--accent);color:var(--bg);font:13px var(--sans);font-weight:500;border-radius:6px;cursor:pointer">24" residential</button>
+          <button type="button" class="w-tab"        data-w="54" style="flex:1;padding:10px 14px;border:1px solid var(--line);background:transparent;color:var(--ink);font:13px var(--sans);font-weight:500;border-radius:6px;cursor:pointer">54" commercial</button>
+        </div>
+        <div style="font:11px var(--sans);text-transform:uppercase;letter-spacing:.08em;color:var(--ink-faint);margin-bottom:10px">Material</div>
+        <div class="material-tabs" id="material-tabs" style="display:flex;flex-wrap:wrap;gap:6px"></div>
+        <div id="material-readout" style="margin-top:10px;font:12px var(--sans);color:var(--ink-soft)"></div>
+      </div>
+
       <div class="detail-spec-panel">
         <div class="spec-row"><span class="spec-key">Category</span><span class="spec-val">${design.category}</span></div>
-        <div class="spec-row"><span class="spec-key">Width</span><span class="spec-val">54"</span></div>
+        <div class="spec-row"><span class="spec-key">Width</span><span class="spec-val" id="spec-width">24"</span></div>
+        <div class="spec-row"><span class="spec-key">Material</span><span class="spec-val" id="spec-material">Paper</span></div>
         <div class="spec-row"><span class="spec-key">Sold by</span><span class="spec-val">Per yard</span></div>
         <div class="spec-row"><span class="spec-key">Repeat</span><span class="spec-val" id="spec-repeat">24" (adjust on left)</span></div>
         <div class="spec-row"><span class="spec-key">Kind</span><span class="spec-val">${design.kind === 'seamless_tile' ? 'Seamless tile' : design.kind}</span></div>
-        <div class="spec-row"><span class="spec-key">Generator</span><span class="spec-val">${design.generator}</span></div>
-        <div class="spec-row"><span class="spec-key">Seed</span><span class="spec-val">${design.seed}</span></div>
+        <div class="spec-row"><span class="spec-key">Designed by</span><span class="spec-val">${design.generator}</span></div>
         <div class="spec-row"><span class="spec-key">Handle</span><span class="spec-val">${design.handle}</span></div>
       </div>
 
+      <script>
+      (function(){
+        var WIDTH_MATERIALS = {
+          '24': [
+            {key:'paper',      label:'Paper',         note:'Traditional non-woven, easy install'},
+            {key:'vinyl',      label:'Vinyl',         note:'Scrubbable, kid-proof, kitchens & baths'},
+            {key:'peelstick',  label:'Peel & Stick',  note:'Rental-friendly, removable, no paste'},
+            {key:'grasscloth', label:'Grasscloth',    note:'Natural fiber, textural, handcrafted'}
+          ],
+          '54': [
+            {key:'vinyl',      label:'Vinyl',         note:'Type II commercial, scrubbable, Class A fire-rated'}
+          ]
+        };
+        var widthTabs = document.querySelectorAll('.w-tab');
+        var matWrap   = document.getElementById('material-tabs');
+        var readout   = document.getElementById('material-readout');
+        var specW     = document.getElementById('spec-width');
+        var specM     = document.getElementById('spec-material');
+
+        var saved = JSON.parse(localStorage.getItem('wallco.material') || 'null') || { w:'24', m:'paper' };
+
+        function paintMaterials(w) {
+          var opts = WIDTH_MATERIALS[w];
+          matWrap.innerHTML = opts.map(function(o){
+            var on = (o.key === saved.m);
+            return '<button type="button" class="m-tab' + (on ? ' is-on' : '') + '" data-m="' + o.key + '" data-note="' + o.note + '" style="padding:8px 14px;border:1px solid ' + (on ? 'var(--accent)' : 'var(--line)') + ';background:' + (on ? 'var(--accent)' : 'transparent') + ';color:' + (on ? 'var(--bg)' : 'var(--ink)') + ';font:12px var(--sans);font-weight:500;border-radius:999px;cursor:pointer">' + o.label + '</button>';
+          }).join('');
+          // If saved.m isn't in this width's options, fall back to first
+          if (!opts.some(function(o){return o.key===saved.m;})) saved.m = opts[0].key;
+          // Wire material buttons
+          matWrap.querySelectorAll('.m-tab').forEach(function(btn){
+            btn.addEventListener('click', function(){
+              saved.m = btn.dataset.m;
+              paintMaterials(w);
+              persist();
+            });
+          });
+          var cur = opts.find(function(o){return o.key===saved.m;}) || opts[0];
+          readout.textContent = cur.note;
+          specW.textContent = w + '"';
+          specM.textContent = cur.label;
+        }
+        function persist(){ localStorage.setItem('wallco.material', JSON.stringify(saved)); }
+
+        widthTabs.forEach(function(t){
+          t.addEventListener('click', function(){
+            saved.w = t.dataset.w;
+            widthTabs.forEach(function(x){
+              var on = x===t;
+              x.classList.toggle('is-on', on);
+              x.style.background = on ? 'var(--accent)' : 'transparent';
+              x.style.color      = on ? 'var(--bg)' : 'var(--ink)';
+              x.style.borderColor= on ? 'var(--accent)' : 'var(--line)';
+            });
+            paintMaterials(saved.w);
+            persist();
+          });
+        });
+        // Restore on load
+        widthTabs.forEach(function(t){
+          if (t.dataset.w === saved.w) t.click();
+        });
+        // If neither matched (shouldn't happen), default to 24"
+        if (!document.querySelector('.w-tab.is-on')) widthTabs[0].click();
+      })();
+      </script>
+
       <!-- Sample CTA (DW standing rule — non-negotiable) -->
       <div class="cta-group">
         <a href="/samples?design=${design.id}" class="btn-primary cta-sample">Request Free Sample</a>

← 1fa03aa copyright: scrub AI vendor names + triple-layer watermark ev  ·  back to Wallco Ai  ·  wallco.ai: motif filter chips on /designs — 12 top motifs (p 6881a12 →