[object Object]

← back to Wallco Ai

yolo-queue: DP-3 + DP-4 ✓ hex swatches + wheel + backend hex mode

df640d3aa02b4bec77a07af575e6dad410a86eab · 2026-05-25 20:15:55 -0700 · Steve Abrams

Files touched

Diff

commit df640d3aa02b4bec77a07af575e6dad410a86eab
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon May 25 20:15:55 2026 -0700

    yolo-queue: DP-3 + DP-4 ✓ hex swatches + wheel + backend hex mode
---
 .yolo-queue.md | 14 ++++++++--
 server.js      | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++++------
 2 files changed, 91 insertions(+), 10 deletions(-)

diff --git a/.yolo-queue.md b/.yolo-queue.md
index 7107b95..09ffca5 100644
--- a/.yolo-queue.md
+++ b/.yolo-queue.md
@@ -39,7 +39,7 @@ never silently pause overnight.
   > into main and other 2 stay as thumbs. Handlers preserved (appendChild
   > moves live element rather than cloning).
 
-- [ ] **DP-3** Replace fashion-house brand buttons in the ADMIN drawer
+- [x] **DP-3** Replace fashion-house brand buttons in the ADMIN drawer
   tone-on-tone block (server.js line ~10854 area, `.recolor-wheel`) with:
   a flat grid of all UNIQUE hex colors aggregated from
   `scripts/fashion_palettes.js` FASHION_PALETTES (de-dup hex strings
@@ -49,7 +49,11 @@ never silently pause overnight.
   with `{hex: '#XXXXXX'}`. NEVER mention brand names in the UI (matches
   Steve's `feedback_never_dw_vendors_in_public_ui` rule).
 
-- [ ] **DP-4** Add `hex` mode to the recolor backend at
+  > NOTE 2026-05-25 ✓ 44 unique hex swatches (circular dots, hex-as-title)
+  > + color wheel input + "Recolor in this hue" run button. 0 brand buttons
+  > remain. Verified via chrome-devtools.
+
+- [x] **DP-4** Add `hex` mode to the recolor backend at
   `app.post('/api/design/:id/recolor')` in server.js (~line 18496). When
   body has `{hex: '#XXXXXX'}`, build a prompt: "Recolor everything as a
   tone-on-tone story using ONLY this hex color #XXXXXX as the dominant
@@ -58,6 +62,12 @@ never silently pause overnight.
   tile, no edge artifacts." Mirror the existing brand/hue mode handling.
   Test with curl after restart.
 
+  > NOTE 2026-05-25 ✓ Hex validation via /^#?([0-9a-fA-F]{6})$/, normalized
+  > uppercase + `#` prefix. Plain-English hue word derived from hue angle
+  > (red/orange/yellow/.../neutral). Prompt builds tone-on-tone story with
+  > 25/35/40 dark/mid/light coverage hints. Smoke-tested: bad hex → 400 w/
+  > hex_format hint, valid hex → 404 (design not found, validation passed).
+
 - [ ] **DP-5** Verify the LEFT vs RIGHT hamburger button positions don't
   overlap on small screens. Currently `.admin-hamburger` is `right:124px`
   when admin (to make room for INFO at 62px), `right:62px` when not
diff --git a/server.js b/server.js
index 7975e24..868c40c 100644
--- a/server.js
+++ b/server.js
@@ -11351,20 +11351,44 @@ ${(() => {
                 if (btn) { btn.disabled = false; btn.style.opacity = '1'; }
               }
             };
+            // DP-3 (2026-05-25): hex-only dispatch. data-hex carries the
+            // exact value, both for grid swatches and the color wheel.
+            // Legacy data-brand / data-hue handlers retained as a fallback
+            // for any leftover buttons elsewhere on the page.
+            document.querySelectorAll('#recolor-buttons button[data-hex]').forEach(b => {
+              b.addEventListener('click', () => fireOne(b.dataset.hex, { hex: b.dataset.hex }, b));
+            });
             document.querySelectorAll('#recolor-buttons button[data-brand], #recolor-buttons button[data-hue]').forEach(b => {
               b.addEventListener('click', () => {
                 if (b.dataset.brand) fireOne(b.dataset.brand, { brand: b.dataset.brand }, b);
                 else                 fireOne(b.dataset.hue,   { hue:   b.dataset.hue   }, b);
               });
             });
+            // Color wheel — pick + run.
+            const wheelInput = document.getElementById('recolor-wheel-input');
+            const wheelReadout = document.getElementById('recolor-wheel-readout');
+            const wheelRun = document.getElementById('recolor-wheel-run');
+            if (wheelInput && wheelReadout) {
+              wheelInput.addEventListener('input', () => {
+                wheelReadout.textContent = wheelInput.value.toUpperCase();
+              });
+            }
+            if (wheelRun && wheelInput) {
+              wheelRun.addEventListener('click', () => {
+                const hex = wheelInput.value.toUpperCase();
+                fireOne(hex, { hex }, wheelRun);
+              });
+            }
             const allBtn = document.getElementById('recolor-all');
             if (allBtn) allBtn.addEventListener('click', async () => {
               allBtn.disabled = true; allBtn.style.opacity = '0.5';
-              const targets = Array.from(document.querySelectorAll('#recolor-buttons button[data-brand], #recolor-buttons button[data-hue]'));
+              const targets = Array.from(document.querySelectorAll('#recolor-buttons button[data-hex], #recolor-buttons button[data-brand], #recolor-buttons button[data-hue]'));
               stat.textContent = 'firing ' + targets.length + ' in parallel…';
               await Promise.all(targets.map(b => {
-                const body = b.dataset.brand ? { brand: b.dataset.brand } : { hue: b.dataset.hue };
-                const label = b.dataset.brand || b.dataset.hue;
+                let body, label;
+                if (b.dataset.hex)        { body = { hex: b.dataset.hex };       label = b.dataset.hex; }
+                else if (b.dataset.brand) { body = { brand: b.dataset.brand };   label = b.dataset.brand; }
+                else                      { body = { hue: b.dataset.hue };       label = b.dataset.hue; }
                 return fireOne(label, body, null);
               }));
               allBtn.disabled = false; allBtn.style.opacity = '1';
@@ -19446,17 +19470,23 @@ app.post('/api/design/:id/recolor', express.json(), async (req, res) => {
     if (!Number.isFinite(id) || id < 1) return res.status(400).json({ ok: false, error: 'bad id' });
     const brandKey = String((req.body && req.body.brand) || '').toLowerCase();
     const hueKey   = String((req.body && req.body.hue)   || '').toLowerCase();
+    // DP-4 (2026-05-25): {hex: '#XXXXXX'} mode. Validates 6-hex format,
+    // normalizes to uppercase, builds a tonal-only prompt around that hex.
+    const rawHex = String((req.body && req.body.hex) || '').trim();
+    const hexMatch = rawHex.match(/^#?([0-9a-fA-F]{6})$/);
+    const hex = hexMatch ? ('#' + hexMatch[1].toUpperCase()) : null;
     const brand = brandKey ? RECOLOR_BRANDS[brandKey] : null;
     const hue   = hueKey   ? RECOLOR_HUES[hueKey]     : null;
-    if (!brand && !hue) {
+    if (!brand && !hue && !hex) {
       return res.status(400).json({
-        ok: false, error: 'bad brand and bad hue',
+        ok: false, error: 'bad brand, hue, or hex',
         valid_brands: Object.keys(RECOLOR_BRANDS),
         valid_hues:   Object.keys(RECOLOR_HUES),
+        hex_format:   '#XXXXXX (6 hex digits)',
       });
     }
-    const targetKey   = brandKey || hueKey;
-    const targetLabel = brand ? brand.label : hue.label;
+    const targetKey   = hex ? hex.slice(1).toLowerCase() : (brandKey || hueKey);
+    const targetLabel = hex ? hex : (brand ? brand.label : hue.label);
 
     // Load source image bytes from local_path or by reading the file behind image_url
     const d = DESIGNS.find(x => x.id === id);
@@ -19523,7 +19553,48 @@ app.post('/api/design/:id/recolor', express.json(), async (req, res) => {
         `Seamless tile, no edge artifacts, no signature, no watermark, no text. ` +
         `High detail, archival quality, fabric-pattern aesthetic — as if printed in ${brand.label}'s own atelier.`;
     }
-    const prompt = brand
+    // DP-4: hex-mode prompt — single hue + tonal range. Plain-English hue
+    // word derived from the hex's hue angle so the prompt is human-readable.
+    let hexPrompt = '';
+    if (hex) {
+      const r = parseInt(hex.slice(1,3),16),
+            g = parseInt(hex.slice(3,5),16),
+            b = parseInt(hex.slice(5,7),16);
+      const max = Math.max(r,g,b), min = Math.min(r,g,b), d = max-min;
+      let hAng = 0;
+      if (d > 0) {
+        if      (max === r) hAng = ((g-b)/d) % 6;
+        else if (max === g) hAng = (b-r)/d + 2;
+        else                hAng = (r-g)/d + 4;
+        hAng *= 60; if (hAng < 0) hAng += 360;
+      }
+      const L = (0.299*r + 0.587*g + 0.114*b) / 255;
+      let hueWord = 'neutral';
+      if (d >= 12) {
+        if (hAng < 15 || hAng >= 345) hueWord = 'red';
+        else if (hAng < 45)  hueWord = 'orange';
+        else if (hAng < 70)  hueWord = 'yellow';
+        else if (hAng < 165) hueWord = 'green';
+        else if (hAng < 200) hueWord = 'teal';
+        else if (hAng < 260) hueWord = 'blue';
+        else if (hAng < 290) hueWord = 'violet';
+        else if (hAng < 330) hueWord = 'magenta';
+        else hueWord = 'pink';
+      } else if (L < 0.2) hueWord = 'near-black charcoal';
+      else if (L > 0.85)  hueWord = 'near-white ivory';
+      else                hueWord = 'mid-grey';
+      hexPrompt =
+        `Recolor everything into a TONE-ON-TONE story built around the single hue ${hex} (${hueWord}). ` +
+        `Use ONLY tonal variants of ${hex} — lighter tints and darker shades of the same hue — NO contrasting hues anywhere. ` +
+        `Distribute: deepest tonal shade as the focal motif silhouette, mid-tone for fill, palest tint as the ground. ` +
+        `Surface coverage roughly: dark ~25%, mid ~35%, light ground ~40%. ` +
+        `Preserve the motif, composition, and pattern repeat exactly — only colors change. ` +
+        `Seamless tile, no edge artifacts, no signature, no watermark, no text. ` +
+        `High detail, archival quality, fabric-pattern aesthetic.`;
+    }
+    const prompt = hex
+      ? hexPrompt
+      : brand
       ? brandPrompt
       : `Recolor everything into the ${hue.label.toUpperCase()} family ONLY. ${hue.tail}. `
         + 'Tone-on-tone: strictly one hue family throughout — no contrasting hues anywhere. '

← 705ca06 design page: Texture tab — 5,034 DW textures, sort + density  ·  back to Wallco Ai  ·  yolo-queue: DP-5 ✓ hamburger media query for mobile 7fc7f07 →