[object Object]

← back to Wallco Ai

T4: keyboard R = regen focused row; visual focus ring; K-key conflict fix

c2332efc51c4b190a894ae4cbc365a4a54ef77f1 · 2026-05-25 03:31:41 -0700 · Steve Abrams

UI:
  - .row.focused now gets a 2px sapphire border + soft ring + inline
    keyboard-hint footer ("⌨ focused — R=regen all, 2/3/4=pick, ...")
    so Steve always sees which row the keymap targets.
  - First undecided row auto-focuses on initial render (no click needed
    to start using the keyboard).

Keymap:
  - R = regenerate ALL variants of the focused row in parallel via the
    existing regenVariant POST. Cost ~$0.12 per press (3 × $0.04).
    Toast announces before + after.
  - Fixed prior K-key double-bind: navigation now checks Shift+K BEFORE
    the plain 'k' branch, so Shift+K (prev row) actually reaches its
    handler. Without this, plain K (keep root) was eating both events.
  - 1/K keep root, 2/3/4 pick A/C/E, J/↓ next, Shift+K/↑ prev — header
    keyhelp updated to match.

Files touched

Diff

commit c2332efc51c4b190a894ae4cbc365a4a54ef77f1
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon May 25 03:31:41 2026 -0700

    T4: keyboard R = regen focused row; visual focus ring; K-key conflict fix
    
    UI:
      - .row.focused now gets a 2px sapphire border + soft ring + inline
        keyboard-hint footer ("⌨ focused — R=regen all, 2/3/4=pick, ...")
        so Steve always sees which row the keymap targets.
      - First undecided row auto-focuses on initial render (no click needed
        to start using the keyboard).
    
    Keymap:
      - R = regenerate ALL variants of the focused row in parallel via the
        existing regenVariant POST. Cost ~$0.12 per press (3 × $0.04).
        Toast announces before + after.
      - Fixed prior K-key double-bind: navigation now checks Shift+K BEFORE
        the plain 'k' branch, so Shift+K (prev row) actually reaches its
        handler. Without this, plain K (keep root) was eating both events.
      - 1/K keep root, 2/3/4 pick A/C/E, J/↓ next, Shift+K/↑ prev — header
        keyhelp updated to match.
---
 public/luxe-curator.html | 98 ++++++++++++++++++++++++++++++++++--------------
 1 file changed, 70 insertions(+), 28 deletions(-)

diff --git a/public/luxe-curator.html b/public/luxe-curator.html
index 878d02b..5aba62d 100644
--- a/public/luxe-curator.html
+++ b/public/luxe-curator.html
@@ -22,10 +22,19 @@
   main { padding:22px 28px 80px; }
   .row {
     display:grid; grid-template-columns:repeat(4, 1fr); gap:12px;
-    margin-bottom:18px; background:var(--card); border:1px solid var(--border);
-    border-radius:10px; padding:12px; transition:opacity .25s;
+    margin-bottom:18px; background:var(--card); border:2px solid var(--border);
+    border-radius:10px; padding:12px; transition:opacity .25s, border-color .15s, box-shadow .15s;
   }
   .row.decided { opacity:0.4; pointer-events:none; }
+  .row.focused {
+    border-color:var(--root);
+    box-shadow:0 0 0 4px rgba(42,77,110,0.12);
+  }
+  .row.focused .meta::after {
+    content:'⌨ focused — R=regen all 3 variants  ·  2/3/4=pick variant  ·  1/K=keep root';
+    display:block; font:10px ui-monospace,Menlo,monospace; color:var(--root);
+    margin-top:4px; letter-spacing:.02em;
+  }
   .row .tile { position:relative; aspect-ratio:1; background:#f0eee8; overflow:hidden; border-radius:6px; cursor:pointer; transition:transform .12s, box-shadow .12s; border:3px solid transparent; }
   .row .tile:hover { transform:scale(1.02); box-shadow:0 4px 14px rgba(0,0,0,0.08); }
   .row .tile.picked { border-color:var(--pick); box-shadow:0 0 0 2px rgba(29,107,58,0.18); }
@@ -90,8 +99,7 @@
     Click <b>"keep root"</b> to reject all 3 variants and keep the original.
   </div>
   <div class="keyhelp">
-    <kbd>1</kbd> root  <kbd>2</kbd> variant A  <kbd>3</kbd> variant C  <kbd>4</kbd> variant E
-    · <kbd>K</kbd> keep root · <kbd>↓</kbd>/<kbd>j</kbd> next row · <kbd>↑</kbd>/<kbd>k</kbd> prev row
+    <kbd>1</kbd>/<kbd>K</kbd> keep root  ·  <kbd>2</kbd> pick A  ·  <kbd>3</kbd> pick C  ·  <kbd>4</kbd> pick E  ·  <kbd>R</kbd> regen all 3 variants in focused row (~$0.12)  ·  <kbd>↓</kbd>/<kbd>J</kbd> next  ·  <kbd>↑</kbd>/<kbd>Shift+K</kbd> prev
   </div>
 </header>
 <main>
@@ -253,55 +261,89 @@ async function keepRoot(rowIdx) {
   } catch (e) { toast('ERR: ' + e.message); }
 }
 
+function setFocus(rowIdx) {
+  // Strip prior focus + apply to new one
+  document.querySelectorAll('.row.focused').forEach(r => r.classList.remove('focused'));
+  const row = document.getElementById('row-' + rowIdx);
+  if (row) {
+    row.classList.add('focused');
+    row.scrollIntoView({ behavior: 'smooth', block: 'center' });
+    FOCUS = rowIdx;
+  }
+}
+
 function advance(rowIdx) {
-  // Scroll to next undecided row
   for (let i = rowIdx + 1; i < DATA.length; i++) {
     const row = document.getElementById('row-' + i);
-    if (row && !row.classList.contains('decided')) {
-      row.scrollIntoView({ behavior: 'smooth', block: 'start' });
-      FOCUS = i;
-      return;
-    }
+    if (row && !row.classList.contains('decided')) { setFocus(i); return; }
   }
 }
 
+// Regenerate ALL 3 variants of the focused row in parallel.
+// Cost: ~$0.12 per call (3 × $0.04 Gemini Flash Image). Each tile shows
+// its own "regenerating…" overlay; failures don't block the others.
+async function regenFocusedRow(rowIdx) {
+  const p = DATA[rowIdx];
+  if (!p || !p.variants?.length) return;
+  toast(`Regenerating all ${p.variants.length} variants for root #${p.root_id} (~10-30s, ~$${(p.variants.length * 0.04).toFixed(2)})`);
+  // Fire all in parallel — each regenVariant is its own POST + tile update
+  await Promise.allSettled(p.variants.map(v => regenVariant(rowIdx, v.variant)));
+  toast(`Done regenerating row #${p.root_id}`);
+}
+
 document.addEventListener('keydown', (e) => {
   if (e.target.matches('input,textarea')) return;
   const p = DATA[FOCUS];
   if (!p) return;
   const key = e.key.toLowerCase();
-  // 1=root, 2=A, 3=C, 4=E (or whichever variants exist in that order)
-  if (key === '1') { keepRoot(FOCUS); e.preventDefault(); }
-  else if (key === 'k') { keepRoot(FOCUS); e.preventDefault(); }
-  else if (key === '2' || key === '3' || key === '4') {
-    const idx = parseInt(key, 10) - 2; // 2→0, 3→1, 4→2
-    const v = p.variants[idx];
-    if (v) { pickVariant(FOCUS, v.new_id, v.variant); e.preventDefault(); }
-  }
-  else if (key === 'j' || key === 'arrowdown') {
-    for (let i = FOCUS + 1; i < DATA.length; i++) {
+  // Navigation FIRST (shift modifies semantics, has to check before plain-key)
+  if ((key === 'k' && e.shiftKey) || key === 'arrowup') {
+    for (let i = FOCUS - 1; i >= 0; i--) {
       if (!document.getElementById('row-' + i).classList.contains('decided')) {
-        document.getElementById('row-' + i).scrollIntoView({ behavior: 'smooth', block: 'start' });
-        FOCUS = i; break;
+        setFocus(i); break;
       }
     }
-    e.preventDefault();
+    e.preventDefault(); return;
   }
-  else if (key === 'k' && e.shiftKey || key === 'arrowup') {
-    for (let i = FOCUS - 1; i >= 0; i--) {
+  if (key === 'j' || key === 'arrowdown') {
+    for (let i = FOCUS + 1; i < DATA.length; i++) {
       if (!document.getElementById('row-' + i).classList.contains('decided')) {
-        document.getElementById('row-' + i).scrollIntoView({ behavior: 'smooth', block: 'start' });
-        FOCUS = i; break;
+        setFocus(i); break;
       }
     }
-    e.preventDefault();
+    e.preventDefault(); return;
+  }
+  // Picks
+  if (key === '1' || key === 'k') { keepRoot(FOCUS); e.preventDefault(); return; }
+  if (key === '2' || key === '3' || key === '4') {
+    const idx = parseInt(key, 10) - 2; // 2→0, 3→1, 4→2
+    const v = p.variants[idx];
+    if (v) { pickVariant(FOCUS, v.new_id, v.variant); e.preventDefault(); }
+    return;
+  }
+  // Regen
+  if (key === 'r') {
+    // shift+R confirms with a smaller toast; plain R goes straight (Steve hates
+    // confirmation prompts on muscle-memory keys per prior feedback)
+    regenFocusedRow(FOCUS);
+    e.preventDefault(); return;
   }
 });
 
+// After any render, focus the first non-decided row so the keyboard works
+// without requiring an explicit click.
+function focusFirstUndecided() {
+  for (let i = 0; i < DATA.length; i++) {
+    const row = document.getElementById('row-' + i);
+    if (row && !row.classList.contains('decided')) { setFocus(i); break; }
+  }
+}
+
 loadData().then(d => {
   DATA = d.items || [];
   document.getElementById('decidedCount').textContent = d.decided || 0;
   render();
+  focusFirstUndecided();
 }).catch(e => {
   document.getElementById('rows').innerHTML = '<div class="empty">Failed to load: ' + e.message + '</div>';
 });

← 05d4109 T3: regenerate-THIS-variant button on /luxe-curator.html  ·  back to Wallco Ai  ·  T6: gen-luxe --verify-subjects flag (off by default, ~$0.000 fbe6091 →