[object Object]

← back to Wallco Ai

design coordinate: collapse paint chips into <details> table + full sort dropdown (client-side)

029a6cf5ca91e11945efaceff0c01c128cf4e414 · 2026-05-13 14:29:06 -0700 · SteveStudio2

Files touched

Diff

commit 029a6cf5ca91e11945efaceff0c01c128cf4e414
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Wed May 13 14:29:06 2026 -0700

    design coordinate: collapse paint chips into <details> table + full sort dropdown (client-side)
---
 server.js | 130 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 126 insertions(+), 4 deletions(-)

diff --git a/server.js b/server.js
index ddba2af..c6721b8 100644
--- a/server.js
+++ b/server.js
@@ -8006,6 +8006,30 @@ ${htmlHeader('/designs')}
       <div class="pairs-well__panel-head">
         <h3 style="font:500 22px/1.2 ui-serif,Georgia,serif;">Stripes &amp; Plaids from our catalog <span class="count" id="pairs-well-catalog-count"></span></h3>
         <div class="pairs-well__controls">
+          <label for="pairs-well-sort">Sort</label>
+          <select id="pairs-well-sort" class="pw-sort"
+            style="padding:5px 10px;border:1px solid #d8d0c0;background:#fff;color:#1a1a1a;border-radius:999px;font:11px ui-sans-serif,system-ui,sans-serif;letter-spacing:.04em;cursor:pointer">
+            <optgroup label="Match quality">
+              <option value="match">Best match (default)</option>
+            </optgroup>
+            <optgroup label="Alphabetical">
+              <option value="title">Title A → Z</option>
+              <option value="title-desc">Title Z → A</option>
+            </optgroup>
+            <optgroup label="Style">
+              <option value="motif">Style / motif</option>
+            </optgroup>
+            <optgroup label="Color">
+              <option value="light-dark">Light → Dark</option>
+              <option value="dark-light">Dark → Light</option>
+              <option value="color-wheel">Color Wheel</option>
+              <option value="vivid">Most Vivid</option>
+              <option value="muted">Most Muted</option>
+            </optgroup>
+            <optgroup label="Other">
+              <option value="random">Random</option>
+            </optgroup>
+          </select>
           <label for="pairs-well-cols-catalog">Columns</label>
           <input id="pairs-well-cols-catalog" type="range" min="3" max="12" value="8" step="1">
           <span class="pairs-well__cols-val" id="pairs-well-cols-catalog-val">8</span>
@@ -8175,6 +8199,30 @@ ${htmlHeader('/designs')}
     .pairs-well__paints { border-top: 1px dashed #ebe4d8; margin-top: 4px; padding-top: 6px; font-size: 10px; line-height: 1.5; color: #444; }
     .pairs-well__paints .label { color: #888; font-weight: 600; margin-right: 4px; letter-spacing:.04em; }
     .pairs-well__paints .row { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+    /* Collapsed paint-match table on each coordinate card. Closed by default
+       so the card surface stays clean — palette dots stay visible above, the
+       full SW/DE/BM/F&B paint NAMES live behind a tap. */
+    .pairs-well__paints-d { border-top: 1px dashed #ebe4d8; margin-top: 4px; padding-top: 6px; font-size: 10px; line-height: 1.4; color: #444; }
+    .pairs-well__paints-d > summary {
+      list-style: none; cursor: pointer; color: #888;
+      font-size: 10px; letter-spacing: .06em; text-transform: uppercase;
+      font-weight: 600; padding: 2px 0;
+      display: inline-flex; align-items: center; gap: 6px;
+      user-select: none;
+    }
+    .pairs-well__paints-d > summary::-webkit-details-marker { display: none; }
+    .pairs-well__paints-d > summary:hover { color: #1a1a1a; }
+    .pairs-well__paints-d .pw-d-caret { display:inline-block; transition: transform .15s ease; font-size: 10px; }
+    .pairs-well__paints-d[open] .pw-d-caret { transform: rotate(180deg); }
+    .pairs-well__paints-tbl { width: 100%; border-collapse: collapse; margin-top: 5px; }
+    .pairs-well__paints-tbl th, .pairs-well__paints-tbl td {
+      text-align: left; padding: 3px 6px 3px 0; vertical-align: middle;
+      font-size: 10px; line-height: 1.3;
+    }
+    .pairs-well__paints-tbl th { font-weight: 600; color: #777; width: 32px; letter-spacing:.04em; }
+    .pairs-well__paints-tbl td { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 0; }
+    .pairs-well__paints-tbl .pw-square { display:inline-block; width: 9px; height: 9px; vertical-align: middle; margin-right: 5px; border: 1px solid rgba(0,0,0,.12); }
+    .pairs-well__paints-tbl .pw-code { color: #999; font-family: ui-monospace, monospace; font-size: 9px; }
     .pairs-well__skeleton { background: linear-gradient(90deg, #eee 0%, #f6f6f6 50%, #eee 100%); background-size: 200% 100%; animation: pwShim 1.2s linear infinite; aspect-ratio: 1 / 1; border-radius: 6px; }
     @keyframes pwShim { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
     .pairs-well__empty { padding: 18px; color: #777; font-size: 14px; grid-column: 1 / -1; }
@@ -8313,7 +8361,7 @@ ${htmlHeader('/designs')}
     function selectedVendors(){var o=[];vendorChecks.forEach(function(c){if(c.checked)o.push(c.value)});return o.length?o:DEFAULT_VENDORS.slice()}
     try{var st=JSON.parse(localStorage.getItem('pw.vendors')||'null');if(Array.isArray(st)&&st.length)vendorChecks.forEach(function(c){c.checked=st.indexOf(c.value)!==-1})}catch(_){}
     function persistVendors(){try{localStorage.setItem('pw.vendors',JSON.stringify(selectedVendors()))}catch(_){}}
-    vendorChecks.forEach(function(c){c.addEventListener('change',function(){persistVendors();if(cachedSource)renderSourcePalette(cachedSource.palette);if(cachedPairs&&!catalogPanel.hidden)renderGrid(catalogGrid,cachedPairs,false)})});
+    vendorChecks.forEach(function(c){c.addEventListener('change',function(){persistVendors();if(cachedSource)renderSourcePalette(cachedSource.palette);if(cachedPairs&&!catalogPanel.hidden)renderGrid(catalogGrid,sortPairs(cachedPairs,currentSort),false)})});
 
     function wireCols(input,valEl,varName,storageKey){function apply(){var n=Math.max(3,Math.min(12,parseInt(input.value,10)||8));valEl.textContent=n;root.style.setProperty('--'+varName,n);try{localStorage.setItem(storageKey,n)}catch(_){}}try{var s=parseInt(localStorage.getItem(storageKey),10);if(!isNaN(s)&&s>=3&&s<=12)input.value=s}catch(_){}input.addEventListener('input',apply);apply()}
     wireCols(catalogCols,catalogColsVal,'cols-catalog','pw.cols.catalog');
@@ -8356,8 +8404,22 @@ ${htmlHeader('/designs')}
       var pb = '';
       if (dom && dom.paints) {
         var vs = selectedVendors();
-        var rows = vs.map(function(v){var pt=dom.paints[v];if(!pt)return '';return '<span class="row"><span class="label">'+esc(VENDOR_SHORT[v])+'</span><span class="pw-square" style="background:'+esc(pt.hex)+'"></span>'+esc(pt.name)+' <span class="pw-code">'+esc(pt.code)+'</span></span>'}).filter(Boolean).join('');
-        if (rows) pb = '<div class="pairs-well__paints">'+rows+'</div>';
+        // Build the paint rows as a 2-column table (Brand · Color) — no per-row
+        // pill label, since the table header already says it. Hex squares stay
+        // so the table is scannable. Wrapped in <details> so the card surface
+        // stays minimal on load — user opens the table when they want it.
+        var rowsTbl = vs.map(function(v){
+          var pt = dom.paints[v]; if (!pt) return '';
+          return '<tr><th>'+esc(VENDOR_SHORT[v])+'</th>'
+            + '<td><span class="pw-square" style="background:'+esc(pt.hex)+'"></span>'
+            + esc(pt.name)+' <span class="pw-code">'+esc(pt.code)+'</span></td></tr>';
+        }).filter(Boolean).join('');
+        if (rowsTbl) {
+          pb = '<details class="pairs-well__paints-d">'
+             +   '<summary>Paint matches <span class="pw-d-caret" aria-hidden="true">▾</span></summary>'
+             +   '<table class="pairs-well__paints-tbl"><tbody>'+rowsTbl+'</tbody></table>'
+             + '</details>';
+        }
       }
       var href = p.url ? esc(p.url) : (p.handle ? 'https://www.designerwallcoverings.com/products/'+esc(p.handle) : '#');
       var aiOnly = isAi
@@ -8386,6 +8448,66 @@ ${htmlHeader('/designs')}
       if (!items || !items.length) { g.innerHTML = '<div class="pairs-well__empty">No coordinates found.</div>'; return; }
       g.innerHTML = items.map(function (p, i) { return renderCard(p, isAi, i); }).join('');
     }
+
+    // ── Client-side sort for the catalog grid. Server returns match-ranked
+    // order ("Best match"); these sorts re-order the cached array in place.
+    function _hex2hsl(hex) {
+      if (!hex || !/^#?[0-9a-fA-F]{6}$/.test(hex.replace('#',''))) return [0,0,0];
+      var c = hex.replace('#','');
+      var r = parseInt(c.slice(0,2),16)/255, g = parseInt(c.slice(2,4),16)/255, b = parseInt(c.slice(4,6),16)/255;
+      var mx = Math.max(r,g,b), mn = Math.min(r,g,b), d = mx-mn;
+      var l = (mx+mn)/2;
+      var s = d === 0 ? 0 : d/(1-Math.abs(2*l-1));
+      var h = 0;
+      if (d) { if (mx===r) h = ((g-b)/d) % 6; else if (mx===g) h = (b-r)/d + 2; else h = (r-g)/d + 4; }
+      h = (h*60+360) % 360;
+      return [h, s, l];
+    }
+    function _firstHex(p) {
+      if (!p || !p.palette || !p.palette.length) return '#888888';
+      var c = p.palette[0]; return typeof c === 'string' ? c : (c && c.hex) || '#888888';
+    }
+    function sortPairs(items, key) {
+      if (!items || !items.length || !key || key === 'match') return items;
+      var arr = items.slice();
+      switch (key) {
+        case 'title':       return arr.sort(function(a,b){ return (a.title||'').localeCompare(b.title||''); });
+        case 'title-desc':  return arr.sort(function(a,b){ return (b.title||'').localeCompare(a.title||''); });
+        case 'motif':       return arr.sort(function(a,b){ return (a.motif||a.style||'').localeCompare(b.motif||b.style||''); });
+        case 'light-dark':  return arr.sort(function(a,b){ return _hex2hsl(_firstHex(b))[2] - _hex2hsl(_firstHex(a))[2]; });
+        case 'dark-light':  return arr.sort(function(a,b){ return _hex2hsl(_firstHex(a))[2] - _hex2hsl(_firstHex(b))[2]; });
+        case 'color-wheel': return arr.sort(function(a,b){
+          var ha = _hex2hsl(_firstHex(a)), hb = _hex2hsl(_firstHex(b));
+          var ba = Math.floor(ha[0]/30), bb = Math.floor(hb[0]/30);
+          return ba !== bb ? ba-bb : ha[2]-hb[2];
+        });
+        case 'vivid':       return arr.sort(function(a,b){ return _hex2hsl(_firstHex(b))[1] - _hex2hsl(_firstHex(a))[1]; });
+        case 'muted':       return arr.sort(function(a,b){ return _hex2hsl(_firstHex(a))[1] - _hex2hsl(_firstHex(b))[1]; });
+        case 'random': {
+          var seed = Math.floor(Date.now() / 60000);
+          var rand = function(n){ var x = Math.sin(seed * 9301 + n * 49297) * 233280; return x - Math.floor(x); };
+          return arr.map(function(x,i){ return { x: x, k: rand(i) }; }).sort(function(a,b){ return a.k - b.k; }).map(function(o){ return o.x; });
+        }
+        default: return arr;
+      }
+    }
+    // Sort dropdown — persist choice in localStorage (per Steve's standing
+    // rule that sort+density choices survive reload).
+    var sortSel = document.getElementById('pairs-well-sort');
+    var currentSort = 'match';
+    try { var s = localStorage.getItem('pw.sort'); if (s) { currentSort = s; if (sortSel) sortSel.value = s; } } catch (_) {}
+    function applySort() {
+      if (!cachedPairs) return;
+      var sorted = sortPairs(cachedPairs, currentSort);
+      renderGrid(catalogGrid, sorted, false);
+    }
+    if (sortSel) {
+      sortSel.addEventListener('change', function () {
+        currentSort = sortSel.value || 'match';
+        try { localStorage.setItem('pw.sort', currentSort); } catch (_) {}
+        applySort();
+      });
+    }
     function renderAiGrid() {
       var all = (customCards || []).concat(cachedAi || []);
       renderGrid(aiGrid, all, true);
@@ -8420,7 +8542,7 @@ ${htmlHeader('/designs')}
           if (!d || d.ok === false) throw new Error((d && d.error) || 'bad payload');
           cachedSource = d.source || null; cachedPairs = d.pairs || [];
           if (cachedSource) renderSourcePalette(cachedSource.palette || []);
-          renderGrid(catalogGrid, cachedPairs, false);
+          renderGrid(catalogGrid, sortPairs(cachedPairs, currentSort), false);
           catalogCount.textContent = '(' + cachedPairs.length + ')';
           if (!cachedPairs.length && (!cachedSource || !cachedSource.palette || !cachedSource.palette.length)) root.hidden = true;
         })

← e52478f design page: admin tone-on-tone color-wheel buttons + /api/d  ·  back to Wallco Ai  ·  marketplace: designer-powered Wallco marketplace (Patternban 27158ce →