[object Object]

← back to Glitterwalls

color: chip dots from product tags; new sorts (light→dark, dark→light, color wheel)

1f42bc075a84e337a895b0d19fa44fd114ffd2ca · 2026-05-08 08:58:06 -0700 · Steve Abrams

Files touched

Diff

commit 1f42bc075a84e337a895b0d19fa44fd114ffd2ca
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri May 8 08:58:06 2026 -0700

    color: chip dots from product tags; new sorts (light→dark, dark→light, color wheel)
---
 public/index.html | 11 ++++++++++-
 server.js         | 44 +++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 53 insertions(+), 2 deletions(-)

diff --git a/public/index.html b/public/index.html
index 89ed675..c190178 100644
--- a/public/index.html
+++ b/public/index.html
@@ -129,6 +129,9 @@ header { position:fixed; top:0; left:0; right:0; display:flex; justify-content:s
 .card .sample-btn { width:100%; padding:10px 14px; background:#fff; color:#000; font-family:inherit; font-size:10px; letter-spacing:0.22em; text-transform:uppercase; font-weight:700; border:0; cursor:pointer; text-align:center; text-decoration:none; transition:all 0.2s; box-sizing:border-box }
 .card .sample-btn.ghost { background:transparent; color:#fff; border:1px solid rgba(255,255,255,0.65) }
 .card .sample-btn:hover { background:var(--accent); color:#fff; border-color:var(--accent) }
+/* Color dots — always visible bottom-left, lift on hover */
+.card .dots { position:absolute; left:10px; bottom:10px; display:flex; gap:4px; z-index:2; pointer-events:none }
+.card .dots .dot { width:12px; height:12px; border-radius:50%; box-shadow:0 0 0 1px rgba(255,255,255,0.6), 0 1px 4px rgba(0,0,0,0.5) }
 
 @media (max-width:1024px) { .grid { grid-template-columns:repeat(min(var(--cols), 4), 1fr) } }
 @media (max-width:720px) { .grid { grid-template-columns:repeat(2, 1fr) } .density { display:none } }
@@ -461,7 +464,10 @@ textarea:focus-visible,
     <label>Sort</label>
     <select id="sortSelect" aria-label="Sort products" style="background:transparent;color:inherit;border:1px solid var(--line,rgba(255,255,255,.2));font:inherit;font-size:11px;letter-spacing:.10em;text-transform:uppercase;padding:4px 8px;cursor:pointer;outline:none;margin-right:14px">
       <option value="newest">Newest</option>
-      <option value="color">Color</option>
+      <option value="light-dark">Light → Dark</option>
+      <option value="dark-light">Dark → Light</option>
+      <option value="wheel">Color Wheel</option>
+      <option value="color">Color (grouped)</option>
       <option value="style">Style</option>
       <option value="sku">SKU A→Z</option>
       <option value="title">Title A→Z</option>
@@ -597,7 +603,10 @@ function cardHTML(p) {
   const handle = p.handle || p.sku || '';
   const dwSampleUrl = 'https://designerwallcoverings.com/products/' + encodeURIComponent(handle) + '#sample';
   const dwRoomUrl = 'https://designerwallcoverings.com/products/' + encodeURIComponent(handle) + '#room';
+  const dots = (p.color_dots || []).slice(0,4)
+    .map(hx => '<span class="dot" style="background:' + escAttr(hx) + '"></span>').join('');
   return '<img loading="' + (eager ? 'eager' : 'lazy') + '"' + (eager ? ' fetchpriority="high"' : '') + ' src="' + escAttr(safeImg(p.image_url)) + '" alt="' + escAttr(p.title) + '">'
+    + (dots ? '<div class="dots">' + dots + '</div>' : '')
     + '<div class="overlay">'
     + '<div class="actions">'
     + '<button class="sample-btn" onclick="event.stopPropagation();dwmOpen(\'Sample\',' + JSON.stringify({sku:p.sku||p.handle, title:p.title, image_url:safeImg(p.image_url)}).replace(/"/g,'&quot;') + ')">Get Sample</button>'
diff --git a/server.js b/server.js
index 06c8341..5c5f1c9 100644
--- a/server.js
+++ b/server.js
@@ -76,16 +76,58 @@ function styleKey(p) {
   for (const s of STYLE_TAGS) if (tags.some(t => t.includes(s))) return s;
   return 'zzz';
 }
+// Hex color lookup for known color tags (used by light/dark + wheel sorts and chip dots)
+const COLOR_HEX = {
+  black:'#0a0a0a',charcoal:'#2a2a2a',gray:'#888',grey:'#888',silver:'#c0c0c0',
+  white:'#fff',ivory:'#fffff0',cream:'#f5e9c8',champagne:'#f0d8a8',beige:'#e7d6b6',sand:'#dccfa6',blonde:'#e8d8a0',
+  brown:'#6b4a2b',chestnut:'#5a3825',umber:'#5a3a1f',tan:'#c9a36a',khaki:'#bda464',honey:'#d6a23c',
+  copper:'#b87333',brass:'#b5a642',bronze:'#9c6b30',gold:'#d4af37',amber:'#c98a32',butterscotch:'#d6943c',saffron:'#d6a93c',ochre:'#b88c3a',mustard:'#caa53d',
+  red:'#c92a2a',coral:'#e87a6b',pink:'#e6a4b4',salmon:'#e08e7c',rose:'#d68a9a',magenta:'#c5358a',fuchsia:'#d63aaf',
+  orange:'#e07a32',peach:'#f0bb96',apricot:'#e0a373',
+  yellow:'#e8c84a',butter:'#f0e1a0',
+  olive:'#7a7a36','olive ':'#7a7a36',
+  green:'#3d8a4f',lime:'#92c84a',mint:'#aedcc1',
+  teal:'#2a8a8a',aqua:'#3ab4b4',turquoise:'#3ab8b0',cyan:'#3acac4',aquamarine:'#9ed5c8',
+  blue:'#3a5fb8',navy:'#1a2c52',indigo:'#37327a',periwinkle:'#8a93d4',
+  purple:'#6a3a8a',violet:'#7a4aa8',lavender:'#b0a3d6',plum:'#693a55',gilver:'#bfb9b3'
+};
+function rgbOfTag(tag) {
+  const k = String(tag||'').toLowerCase().trim();
+  for (const name of Object.keys(COLOR_HEX)) if (k.includes(name)) return COLOR_HEX[name];
+  return null;
+}
+function hexToRgb(h){const m=/^#?([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i.exec(h||'');return m?[parseInt(m[1],16),parseInt(m[2],16),parseInt(m[3],16)]:null;}
+function rgbToHsl(r,g,b){r/=255;g/=255;b/=255;const mx=Math.max(r,g,b),mn=Math.min(r,g,b);let h=0,s=0,l=(mx+mn)/2;if(mx!==mn){const d=mx-mn;s=l>.5?d/(2-mx-mn):d/(mx+mn);switch(mx){case r:h=(g-b)/d+(g<b?6:0);break;case g:h=(b-r)/d+2;break;case b:h=(r-g)/d+4;break;}h*=60;}return[h,s,l];}
+function dominantHex(p){
+  const tags=(p.tags||[]).map(t=>String(t).toLowerCase());
+  for (const t of tags){const hx=rgbOfTag(t);if(hx)return hx;}
+  return '#888';
+}
+// Up to 4 distinct color dots from tags
+function colorDots(p){
+  const seen=new Set();const out=[];
+  for (const t of (p.tags||[])){const hx=rgbOfTag(t);if(hx&&!seen.has(hx)){seen.add(hx);out.push(hx);if(out.length>=4)break;}}
+  return out;
+}
+function luminance(p){const rgb=hexToRgb(dominantHex(p));return rgb?(0.2126*rgb[0]+0.7152*rgb[1]+0.0722*rgb[2]):128;}
+function hue(p){const rgb=hexToRgb(dominantHex(p));if(!rgb)return 999;const [h,s]=rgbToHsl(rgb[0],rgb[1],rgb[2]);return s<0.08?999:h;}
+
 function sortProducts(list, mode) {
   if (mode === 'sku') return [...list].sort((a,b) => String(a.sku || a.handle || '').localeCompare(String(b.sku || b.handle || '')));
   if (mode === 'title') return [...list].sort((a,b) => String(a.title || '').localeCompare(String(b.title || '')));
   if (mode === 'color') return [...list].sort((a,b) => colorRank(a) - colorRank(b) || String(a.title || '').localeCompare(String(b.title || '')));
   if (mode === 'style') return [...list].sort((a,b) => styleKey(a).localeCompare(styleKey(b)) || String(a.title || '').localeCompare(String(b.title || '')));
+  if (mode === 'light-dark') return [...list].sort((a,b) => luminance(b) - luminance(a)); // bright first
+  if (mode === 'dark-light') return [...list].sort((a,b) => luminance(a) - luminance(b)); // dark first
+  if (mode === 'wheel') return [...list].sort((a,b) => hue(a) - hue(b));
   if (mode === 'price-asc')  return [...list].sort((a,b) => (Number(a.max_price) || 0) - (Number(b.max_price) || 0));
   if (mode === 'price-desc') return [...list].sort((a,b) => (Number(b.max_price) || 0) - (Number(a.max_price) || 0));
   return list;
 }
 
+// Decorate every product with color_dots + dominant_hex so the frontend can render.
+function decorateColors(items){return items.map(p=>({...p,color_dots:colorDots(p),dominant_hex:dominantHex(p)}));}
+
 
 app.use(express.json({ limit: "256kb" }));
 const cfg = require('../_shared/site-config').load(__dirname);
@@ -107,7 +149,7 @@ app.get('/api/products', (req, res) => {
   const pageNum = Math.max(1, parseInt(page) || 1);
   const lim = Math.min(60, parseInt(limit) || 24);
   const start = (pageNum - 1) * lim;
-  res.json({ total, page: pageNum, limit: lim, pages: Math.ceil(total/lim), sort, items: list.slice(start, start + lim) });
+  res.json({ total, page: pageNum, limit: lim, pages: Math.ceil(total/lim), sort, items: decorateColors(list.slice(start, start + lim)) });
   // (replaced) old: pages: Math.ceil(total / lim), items: list.slice(start, start + lim) });
 });
 

← 91b6213 chips: image-only by default; hover reveals Get Sample + See  ·  back to Glitterwalls  ·  sort modal works + 142/143 SKUs have real-pixel hex via sips 8a2f994 →