[object Object]

← back to Wallco Ai

new.html: add Sort (6 opts incl color-by-hue) + Density slider per standing rule — both persist to localStorage [yolo tick1, local-only]

7aa964558e40a6b9feaf950f500ceac4cc239d4e · 2026-06-02 10:52:25 -0700 · Steve Abrams

Files touched

Diff

commit 7aa964558e40a6b9feaf950f500ceac4cc239d4e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jun 2 10:52:25 2026 -0700

    new.html: add Sort (6 opts incl color-by-hue) + Density slider per standing rule — both persist to localStorage [yolo tick1, local-only]
---
 public/new.html | 109 +++++++++++++++++++++++++++++++++++++++++---------------
 1 file changed, 81 insertions(+), 28 deletions(-)

diff --git a/public/new.html b/public/new.html
index fb3c8ea..a1deeb9 100644
--- a/public/new.html
+++ b/public/new.html
@@ -8,6 +8,7 @@
 <style>
   :root {
     --ink:#1f1808; --line:#d8d0c0; --gold:#c9a14b;
+    --card-min:220px;
     --bg:#fbf8f1; --card:#fff; --faint:#7a6e5a;
     --green:#1f6a2c; --blue:#3b78d8; --purple:#7a4ab8; --orange:#c0660b; --red:#b3261e;
     --sans:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
@@ -49,8 +50,9 @@
   }
 
   .grid {
-    display:grid; grid-template-columns:repeat(auto-fill,minmax(220px,1fr)); gap:14px;
+    display:grid; grid-template-columns:repeat(auto-fill,minmax(var(--card-min),1fr)); gap:14px;
   }
+  .controls input[type=range] { accent-color:var(--gold); vertical-align:middle; }
   .card {
     background:var(--card); border:1px solid var(--line); border-radius:8px;
     overflow:hidden; cursor:pointer; transition:transform .12s, box-shadow .12s;
@@ -118,6 +120,17 @@
       <option value="48">last 48h</option>
       <option value="168">last 7d</option>
     </select>
+    <label>Sort</label>
+    <select id="sort">
+      <option value="newest">Newest</option>
+      <option value="oldest">Oldest</option>
+      <option value="category">Category A→Z</option>
+      <option value="color">Color (by hue)</option>
+      <option value="id">ID ↑</option>
+      <option value="id_desc">ID ↓</option>
+    </select>
+    <label>Density</label>
+    <input type="range" id="density" min="140" max="340" step="10" title="card size">
     <span class="right" id="meta">—</span>
     <a href="/admin/ghost-review">🔍 Ghost Review →</a>
   </div>
@@ -134,8 +147,43 @@
   const $ = (id) => document.getElementById(id);
   let HOURS = 24;
   let LAST_TOTAL = 0;
+  let ALL = [];
   $('hours').onchange = (e) => { HOURS = parseInt(e.target.value, 10); refresh(); };
 
+  // sort + density (Steve standing rule: every product grid; both persist)
+  $('sort').value = localStorage.getItem('new_sort') || 'newest';
+  $('density').value = localStorage.getItem('new_density') || '220';
+  document.documentElement.style.setProperty('--card-min', $('density').value + 'px');
+  $('sort').onchange = () => { localStorage.setItem('new_sort', $('sort').value); render(); };
+  $('density').oninput = () => {
+    localStorage.setItem('new_density', $('density').value);
+    document.documentElement.style.setProperty('--card-min', $('density').value + 'px');
+  };
+
+  function hexHue(hex) {
+    const m = /^#?([0-9a-fA-F]{6})$/.exec(hex || ''); if (!m) return 1e6;
+    const n = parseInt(m[1], 16), r = (n >> 16 & 255) / 255, g = (n >> 8 & 255) / 255, b = (n & 255) / 255;
+    const max = Math.max(r, g, b), min = Math.min(r, g, b), d = max - min; let h = 0;
+    if (d === 0) h = 0;
+    else if (max === r) h = ((g - b) / d) % 6;
+    else if (max === g) h = (b - r) / d + 2;
+    else h = (r - g) / d + 4;
+    h *= 60; if (h < 0) h += 360;
+    const s = max === 0 ? 0 : d / max;
+    if (s < 0.12) return 1e5 + (1 - max);
+    return h;
+  }
+  function sortItems(rows) {
+    const r = rows.slice(), m = $('sort').value;
+    if (m === 'newest') r.sort((a, b) => String(b.created_at || '').localeCompare(String(a.created_at || '')));
+    else if (m === 'oldest') r.sort((a, b) => String(a.created_at || '').localeCompare(String(b.created_at || '')));
+    else if (m === 'category') r.sort((a, b) => String(a.category || '').localeCompare(String(b.category || '')) || (b.id - a.id));
+    else if (m === 'color') r.sort((a, b) => hexHue(a.dominant_hex) - hexHue(b.dominant_hex) || (b.id - a.id));
+    else if (m === 'id') r.sort((a, b) => a.id - b.id);
+    else if (m === 'id_desc') r.sort((a, b) => b.id - a.id);
+    return r;
+  }
+
   function genKind(gen) {
     const g = (gen || '').toLowerCase();
     if (g.includes('smart-fix') || g.includes('smart_fix')) return 'smart-fix';
@@ -158,6 +206,7 @@
         setTimeout(() => { document.title = "What's New — wallco.ai"; }, 4000);
       }
       LAST_TOTAL = items.length;
+      ALL = items;
 
       const counts = { 'smart-fix':0, 'crop-fix':0, 'regen':0, 'wand':0, 'other':0 };
       items.forEach(it => { counts[genKind(it.generator)]++; });
@@ -168,38 +217,42 @@
       $('stat-regen').textContent = counts['regen'];
       $('meta').textContent = `${items.length} shown · window=${HOURS}h · auto-refresh 10s`;
 
-      const grid = $('grid');
-      grid.innerHTML = '';
-      if (!items.length) { $('empty').style.display = ''; return; }
-      $('empty').style.display = 'none';
-      const frag = document.createDocumentFragment();
-      for (const it of items) {
-        const k = genKind(it.generator);
-        const card = document.createElement('a');
-        card.className = 'card';
-        card.href = `/design/${it.id}`;
-        card.target = '_blank';
-        const ts = new Date(it.created_at);
-        const ago = humanAgo(ts);
-        card.innerHTML = `
-          <div class="thumb">
-            <img loading="lazy" src="/designs/img/by-id/${it.id}" alt="${it.id}">
-            <div class="id">#${it.id}</div>
-            <div class="gen-pill gen-${k}">${pillLabel(k)}</div>
-          </div>
-          <div class="meta-row">
-            <span class="cat">${it.category || '—'}</span>
-            <span class="ts">${ago} · ${it.dominant_hex || ''}</span>
-          </div>
-        `;
-        frag.appendChild(card);
-      }
-      grid.appendChild(frag);
+      render();
     } catch (e) {
       console.warn('refresh failed:', e.message);
     }
   }
 
+  function render() {
+    const grid = $('grid');
+    grid.innerHTML = '';
+    if (!ALL.length) { $('empty').style.display = ''; return; }
+    $('empty').style.display = 'none';
+    const frag = document.createDocumentFragment();
+    for (const it of sortItems(ALL)) {
+      const k = genKind(it.generator);
+      const card = document.createElement('a');
+      card.className = 'card';
+      card.href = `/design/${it.id}`;
+      card.target = '_blank';
+      const ts = new Date(it.created_at);
+      const ago = humanAgo(ts);
+      card.innerHTML = `
+        <div class="thumb">
+          <img loading="lazy" src="/designs/img/by-id/${it.id}" alt="${it.id}">
+          <div class="id">#${it.id}</div>
+          <div class="gen-pill gen-${k}">${pillLabel(k)}</div>
+        </div>
+        <div class="meta-row">
+          <span class="cat">${it.category || '—'}</span>
+          <span class="ts">${ago} · ${it.dominant_hex || ''}</span>
+        </div>
+      `;
+      frag.appendChild(card);
+    }
+    grid.appendChild(frag);
+  }
+
   function humanAgo(ts) {
     const sec = Math.max(0, Math.round((Date.now() - ts.getTime()) / 1000));
     if (sec < 60)    return `${sec}s ago`;

← 4eebff7 Add settlement-gate-batch.js: gate ungated designs by catego  ·  back to Wallco Ai  ·  yolo-backlog: open 2026-06-02 refinement loop (local-only pe 6452ad3 →