[object Object]

← back to Wallco Ai

designs: studio adjust panel (6 photoshop sliders) + per-card favorites

753227db5f7c7eba1b3ee358bb56e05f20af4693 · 2026-05-13 10:56:38 -0700 · Steve

Studio Adjust — global collapsible panel on /designs with 6 sliders:
  Hue (hue-rotate -180→180°)
  Saturation (saturate 0→200%)
  Brightness (brightness 40→160%)
  Contrast (contrast 40→200%)
  Warmth (sepia 0→100% — warm wash)
  Sharpness (positive = contrast+saturate boost, negative = blur)
Slider values apply via CSS filter() to every .card-img on the page.
Persists in localStorage('wallco.designs.adjust'). Reset button restores defaults.

Favorites — heart button (♡/♥) on every public card. Click toggles
localStorage('wallco.favorites') = JSON array of design ids. New 'Favorites'
filter chip in the toolbar — when active, hides cards not in the favorites
set and shows an empty state if none saved yet.

Replaces nothing — the existing admin-rating sliders (Design/Color/Style 0-10
on review-cards) stay intact since they drive the RLHF feedback pipeline.
Public users now get image-adjust sliders + favorite collection instead.

Files touched

Diff

commit 753227db5f7c7eba1b3ee358bb56e05f20af4693
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed May 13 10:56:38 2026 -0700

    designs: studio adjust panel (6 photoshop sliders) + per-card favorites
    
    Studio Adjust — global collapsible panel on /designs with 6 sliders:
      Hue (hue-rotate -180→180°)
      Saturation (saturate 0→200%)
      Brightness (brightness 40→160%)
      Contrast (contrast 40→200%)
      Warmth (sepia 0→100% — warm wash)
      Sharpness (positive = contrast+saturate boost, negative = blur)
    Slider values apply via CSS filter() to every .card-img on the page.
    Persists in localStorage('wallco.designs.adjust'). Reset button restores defaults.
    
    Favorites — heart button (♡/♥) on every public card. Click toggles
    localStorage('wallco.favorites') = JSON array of design ids. New 'Favorites'
    filter chip in the toolbar — when active, hides cards not in the favorites
    set and shows an empty state if none saved yet.
    
    Replaces nothing — the existing admin-rating sliders (Design/Color/Style 0-10
    on review-cards) stay intact since they drive the RLHF feedback pipeline.
    Public users now get image-adjust sliders + favorite collection instead.
---
 server.js | 138 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 138 insertions(+)

diff --git a/server.js b/server.js
index 0ba823f..11435f3 100644
--- a/server.js
+++ b/server.js
@@ -3388,6 +3388,144 @@ ${FOOTER}
     });
   }
 
+  // ── Studio Adjust — 6 Photoshop-style sliders applied globally via CSS filter().
+  //    Persists in localStorage('wallco.designs.adjust'). Affects every .card-img.
+  (function studioAdjust(){
+    var KEY = 'wallco.designs.adjust';
+    var defaults = { hue:0, sat:100, bri:100, con:100, warm:0, sharp:0 };
+    var state = (function(){ try { return Object.assign({}, defaults, JSON.parse(localStorage.getItem(KEY)||'{}')); } catch(e){ return Object.assign({}, defaults); } })();
+    var rows = document.querySelectorAll('#studio-adjust .sa-row');
+    var summary = document.getElementById('studio-adjust-summary');
+    var resetBtn = document.getElementById('studio-adjust-reset');
+
+    function buildFilter(){
+      // Warmth = sepia approximation (warm wash). Sharpness positive = saturation/contrast micro-boost;
+      // negative = soft blur. Keeps semantics user-friendly without exotic SVG filters.
+      var sharpExtra = '';
+      if (state.sharp < 0) sharpExtra = ' blur(' + (Math.abs(state.sharp) * 0.4) + 'px)';
+      else if (state.sharp > 0) sharpExtra = ' contrast(' + (100 + state.sharp * 4) + '%) saturate(' + (100 + state.sharp * 4) + '%)';
+      return 'hue-rotate(' + state.hue + 'deg) saturate(' + state.sat + '%) brightness(' + state.bri + '%) contrast(' + state.con + '%) sepia(' + state.warm + '%)' + sharpExtra;
+    }
+    function applyToAll(){
+      var f = buildFilter();
+      var imgs = document.querySelectorAll('.design-card .card-img');
+      for (var i=0;i<imgs.length;i++) imgs[i].style.filter = f;
+      var changed = state.hue||0!==0 || state.sat!==100 || state.bri!==100 || state.con!==100 || state.warm!==0 || state.sharp!==0;
+      if (summary) summary.textContent = changed ? 'on' : 'defaults';
+    }
+    function syncRow(row){
+      var axis = row.getAttribute('data-axis');
+      var inp = row.querySelector('input[type=range]');
+      var val = row.querySelector('.sa-val');
+      inp.value = state[axis];
+      var suffix = axis==='hue' ? '°' : (axis==='sharp' ? '' : '%');
+      val.textContent = state[axis] + suffix;
+    }
+    rows.forEach(function(row){
+      var axis = row.getAttribute('data-axis');
+      var inp = row.querySelector('input[type=range]');
+      var val = row.querySelector('.sa-val');
+      syncRow(row);
+      inp.addEventListener('input', function(){
+        state[axis] = parseFloat(this.value);
+        var suffix = axis==='hue' ? '°' : (axis==='sharp' ? '' : '%');
+        val.textContent = state[axis] + suffix;
+        applyToAll();
+        localStorage.setItem(KEY, JSON.stringify(state));
+      });
+    });
+    if (resetBtn) resetBtn.addEventListener('click', function(){
+      state = Object.assign({}, defaults);
+      rows.forEach(syncRow);
+      applyToAll();
+      localStorage.setItem(KEY, JSON.stringify(state));
+    });
+    applyToAll();
+  })();
+
+  // ── Favorites — heart button per card, "Favorites only" filter chip.
+  //    Persists in localStorage('wallco.favorites') as a JSON array of design ids.
+  (function favorites(){
+    var FKEY = 'wallco.favorites';
+    function readSet(){
+      try { return new Set(JSON.parse(localStorage.getItem(FKEY) || '[]').map(String)); }
+      catch(e) { return new Set(); }
+    }
+    function writeSet(set){
+      localStorage.setItem(FKEY, JSON.stringify(Array.from(set).map(Number)));
+    }
+    var favs = readSet();
+    function setBtnState(btn, isFav){
+      btn.setAttribute('aria-pressed', isFav ? 'true' : 'false');
+      btn.classList.toggle('is-fav', isFav);
+    }
+    function hydrateButtons(){
+      document.querySelectorAll('.fav-btn').forEach(function(btn){
+        var id = btn.getAttribute('data-fav-id');
+        setBtnState(btn, favs.has(id));
+      });
+      updateFavCount();
+    }
+    function updateFavCount(){
+      var n = favs.size;
+      var c = document.getElementById('fav-only-count');
+      if (c) c.textContent = '·' + n;
+      var extra = document.getElementById('fav-count-extra');
+      if (extra) extra.textContent = n ? '· ' + n + ' favorited' : '';
+    }
+    function applyFavFilter(showOnlyFavs){
+      var cards = document.querySelectorAll('.design-card[data-design-id]');
+      var anyVisible = false;
+      cards.forEach(function(card){
+        var id = card.getAttribute('data-design-id');
+        var match = !showOnlyFavs || favs.has(id);
+        card.style.display = match ? '' : 'none';
+        if (match) anyVisible = true;
+      });
+      var grid = document.getElementById('catalog-grid');
+      var empty = document.getElementById('fav-empty-state');
+      if (showOnlyFavs && !anyVisible) {
+        if (!empty && grid) {
+          empty = document.createElement('div');
+          empty.id = 'fav-empty-state';
+          empty.style.cssText = 'grid-column:1/-1;padding:48px 24px;text-align:center;color:var(--ink-soft,#666);font:14px/1.6 var(--sans,system-ui)';
+          empty.innerHTML = '<div style="font-size:32px;margin-bottom:8px">♡</div><strong>No favorites yet.</strong><br>Tap the heart on any design to save it here.';
+          grid.appendChild(empty);
+        }
+      } else if (empty) {
+        empty.remove();
+      }
+    }
+    document.addEventListener('click', function(e){
+      var btn = e.target.closest && e.target.closest('.fav-btn');
+      if (!btn) return;
+      e.preventDefault();
+      e.stopPropagation();
+      var id = btn.getAttribute('data-fav-id');
+      if (favs.has(id)) favs.delete(id); else favs.add(id);
+      writeSet(favs);
+      setBtnState(btn, favs.has(id));
+      updateFavCount();
+      var toggle = document.getElementById('fav-only-toggle');
+      if (toggle && toggle.getAttribute('aria-pressed') === 'true') {
+        applyFavFilter(true);
+      }
+    });
+    var favToggle = document.getElementById('fav-only-toggle');
+    if (favToggle) {
+      favToggle.addEventListener('click', function(){
+        var on = this.getAttribute('aria-pressed') !== 'true';
+        this.setAttribute('aria-pressed', on ? 'true' : 'false');
+        this.style.background = on ? 'var(--accent,#c9a14b)' : 'transparent';
+        this.style.color = on ? 'var(--bg,#fff)' : 'var(--ink-soft)';
+        this.style.borderColor = on ? 'var(--accent,#c9a14b)' : 'var(--line)';
+        this.innerHTML = (on ? '♥' : '♡') + ' Favorites <span id="fav-only-count" style="opacity:.55">·' + favs.size + '</span>';
+        applyFavFilter(on);
+      });
+    }
+    hydrateButtons();
+  })();
+
   // Live search dropdown — hits /api/designs/search, falls back to form submit on Enter / no-JS.
   var sIn = document.getElementById('catalog-search-input');
   var sBox = document.getElementById('catalog-search-suggest');

← f2b894b mural-scenic: $950 infinite license + 150dpi download (Strip  ·  back to Wallco Ai  ·  designs: sort buttons (was dropdown) + /merge upload-and-fus d7d74bc →