← 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
M public/luxe-curator.html
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 →