[object Object]

← back to CelebritySignatures

wear: add typeahead search across the top of the signature picker

a70566c1bc92c19e1994a0aa62d9125a7364118f · 2026-09-09 10:20:46 -0700 · Steve

Full-width search bar above the grid that (a) live-filters the 2,955-signature
grid as you type (name + category) with the count + empty-state updating, and
(b) drops an autocomplete list of the top ranked matches (word-start > substring,
match highlighted, thumbnail + category) that you can click or arrow-key +Enter
to jump straight to the product. Escape/blur closes it; × clears. Pure client-side
addition to wear.html, no server change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013oMdD46CVyPsuM6JUoiea7

Files touched

Diff

commit a70566c1bc92c19e1994a0aa62d9125a7364118f
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Sep 9 10:20:46 2026 -0700

    wear: add typeahead search across the top of the signature picker
    
    Full-width search bar above the grid that (a) live-filters the 2,955-signature
    grid as you type (name + category) with the count + empty-state updating, and
    (b) drops an autocomplete list of the top ranked matches (word-start > substring,
    match highlighted, thumbnail + category) that you can click or arrow-key +Enter
    to jump straight to the product. Escape/blur closes it; × clears. Pure client-side
    addition to wear.html, no server change.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_013oMdD46CVyPsuM6JUoiea7
---
 public/wear.html                  |  92 +++++++++++++++++++++++++++++++++++++-
 verification/search-typeahead.png | Bin 0 -> 79778 bytes
 2 files changed, 91 insertions(+), 1 deletion(-)

diff --git a/public/wear.html b/public/wear.html
index 0333f65..b5539c4 100644
--- a/public/wear.html
+++ b/public/wear.html
@@ -53,6 +53,21 @@
   .card .nm { font-size:13px; margin-top:8px; font-weight:500; }
   .card .cat { font-size:11px; color:#9a978f; text-transform:uppercase; letter-spacing:.05em; }
   .empty { background:#fff; border:1px dashed var(--line); border-radius:12px; padding:44px 24px; text-align:center; color:#6b6b6b; }
+  /* signature search + typeahead */
+  .searchbar { position:relative; margin:18px 0 4px; }
+  .searchbar .ico { position:absolute; left:14px; top:50%; transform:translateY(-50%); color:#9a978f; font-size:16px; pointer-events:none; }
+  #sigSearch { width:100%; font:inherit; font-size:16px; padding:13px 42px 13px 40px; border:1px solid var(--line); border-radius:12px; background:#fff; color:var(--ink); }
+  #sigSearch:focus { outline:none; border-color:var(--ink); box-shadow:0 4px 18px rgba(0,0,0,.06); }
+  .searchbar .clear { position:absolute; right:10px; top:50%; transform:translateY(-50%); border:0; background:none; color:#9a978f; font-size:20px; cursor:pointer; padding:2px 8px; line-height:1; display:none; }
+  .searchbar .clear:hover { color:var(--ink); }
+  .ac { position:absolute; z-index:20; left:0; right:0; top:calc(100% + 6px); background:#fff; border:1px solid var(--line); border-radius:12px; box-shadow:0 12px 30px rgba(0,0,0,.12); overflow:hidden; display:none; max-height:340px; overflow-y:auto; }
+  .ac-item { display:flex; align-items:center; gap:12px; padding:9px 14px; cursor:pointer; border-bottom:1px solid var(--paper); }
+  .ac-item:last-child { border-bottom:0; }
+  .ac-item.active, .ac-item:hover { background:var(--paper); }
+  .ac-item img { width:40px; height:34px; object-fit:contain; mix-blend-mode:multiply; flex:0 0 40px; }
+  .ac-item .ac-nm { font-size:14px; font-weight:500; }
+  .ac-item .ac-nm b { color:var(--green); font-weight:600; }
+  .ac-item .ac-cat { font-size:11px; color:#9a978f; text-transform:uppercase; letter-spacing:.05em; margin-left:auto; }
   /* product overlay */
   .scrim { position:fixed; inset:0; background:rgba(20,18,14,.5); display:none; align-items:center; justify-content:center; padding:20px; z-index:40; }
   .scrim.on { display:flex; }
@@ -107,6 +122,15 @@
   <div id="disclosure" class="disclosure">Loading trademark screen…</div>
   <p id="selectionNotice" class="disclosure warn" role="status" hidden></p>
 
+  <div class="searchbar">
+    <span class="ico">🔍</span>
+    <input id="sigSearch" type="text" autocomplete="off" spellcheck="false"
+           placeholder="Search a name — Lincoln, Einstein, Marilyn Monroe…"
+           role="combobox" aria-expanded="false" aria-autocomplete="list" aria-controls="ac">
+    <button class="clear" id="sigClear" aria-label="Clear search">×</button>
+    <div class="ac" id="ac" role="listbox"></div>
+  </div>
+
   <div class="controls">
     <label>Sort
       <select id="sort">
@@ -256,6 +280,8 @@ async function boot(){
 function render(){
   const grid = document.getElementById('grid'), empty = document.getElementById('empty');
   let sigs = (state.data.signatures||[]).slice();
+  const q = (state.q||'').trim().toLowerCase();
+  if (q) sigs = sigs.filter(s => (s.full_name||'').toLowerCase().includes(q) || (s.category||'').toLowerCase().includes(q));
   const sort = document.getElementById('sort').value;
   if (sort==='name') sigs.sort((a,b)=>a.full_name.localeCompare(b.full_name));
   else if (sort==='cat') sigs.sort((a,b)=>(a.category||'').localeCompare(b.category||'')||a.full_name.localeCompare(b.full_name));
@@ -263,7 +289,9 @@ function render(){
   document.getElementById('count').textContent = sigs.length ? `${sigs.length.toLocaleString()} available` : '';
   if (!sigs.length){
     grid.style.display='none'; empty.style.display='block';
-    empty.innerHTML = 'No signatures matched the eligibility gate.';
+    empty.innerHTML = (state.q||'').trim()
+      ? `No signatures match “${(state.q).replace(/</g,'&lt;')}”.`
+      : 'No signatures matched the eligibility gate.';
     return;
   }
   grid.style.display='grid'; empty.style.display='none';
@@ -589,6 +617,68 @@ function draw(){
 })();
 
 document.getElementById('sort').onchange = e => { localStorage.setItem('wear_sort', e.target.value); render(); };
+
+// ---- search + typeahead over the signature names ----
+(function initSearch(){
+  const input = document.getElementById('sigSearch');
+  const clear = document.getElementById('sigClear');
+  const ac    = document.getElementById('ac');
+  let items = [], active = -1;
+
+  const esc = t => String(t).replace(/[&<>]/g, c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));
+  const hl  = (name, q) => { // bold the matched substring
+    const i = name.toLowerCase().indexOf(q); if (i<0) return esc(name);
+    return esc(name.slice(0,i))+'<b>'+esc(name.slice(i,i+q.length))+'</b>'+esc(name.slice(i+q.length));
+  };
+  function closeAc(){ ac.style.display='none'; input.setAttribute('aria-expanded','false'); active=-1; items=[]; }
+  function suggest(q){
+    const all = state.data && state.data.signatures || [];
+    const scored = [];
+    for (const s of all){
+      const n = (s.full_name||'').toLowerCase(); const i = n.indexOf(q);
+      if (i<0) continue;
+      // prefix / word-start matches rank first, then earliest position
+      const wordStart = i===0 || n[i-1]===' ';
+      scored.push({ s, rank: (i===0?0:wordStart?1:2), pos:i });
+      if (scored.length>400) break; // cap the scan work
+    }
+    scored.sort((a,b)=> a.rank-b.rank || a.pos-b.pos || a.s.full_name.localeCompare(b.s.full_name));
+    return scored.slice(0,8).map(x=>x.s);
+  }
+  function openAc(q){
+    items = suggest(q);
+    if (!items.length){ closeAc(); return; }
+    ac.innerHTML = items.map((s,i)=>`<div class="ac-item${i===active?' active':''}" role="option" data-qid="${s.qid}">
+      <img src="${s.signature_image_url}" alt="" loading="lazy" onerror="this.style.visibility='hidden'">
+      <span class="ac-nm">${hl(s.full_name||'', q)}</span><span class="ac-cat">${esc(s.category||'')}</span></div>`).join('');
+    ac.style.display='block'; input.setAttribute('aria-expanded','true');
+    ac.querySelectorAll('.ac-item').forEach(el=>{
+      el.onmousedown = ev => { ev.preventDefault(); pick(el.dataset.qid); };
+    });
+  }
+  function pick(qid){ closeAc(); input.blur(); openProduct(qid); }
+  function onType(){
+    const raw = input.value; state.q = raw; clear.style.display = raw ? 'block':'none';
+    const q = raw.trim().toLowerCase();
+    render();                       // live-filter the grid
+    if (q) openAc(q); else closeAc();
+  }
+  input.addEventListener('input', onType);
+  input.addEventListener('focus', ()=>{ const q=input.value.trim().toLowerCase(); if(q) openAc(q); });
+  input.addEventListener('blur',  ()=> setTimeout(closeAc,120));
+  input.addEventListener('keydown', e=>{
+    if (ac.style.display!=='block') return;
+    if (e.key==='ArrowDown'){ e.preventDefault(); active=Math.min(active+1,items.length-1); paintActive(); }
+    else if (e.key==='ArrowUp'){ e.preventDefault(); active=Math.max(active-1,0); paintActive(); }
+    else if (e.key==='Enter'){ e.preventDefault(); const s=items[active>=0?active:0]; if(s) pick(s.qid); }
+    else if (e.key==='Escape'){ closeAc(); }
+  });
+  function paintActive(){
+    ac.querySelectorAll('.ac-item').forEach((el,i)=>el.classList.toggle('active',i===active));
+    const el = ac.querySelector('.ac-item.active'); if(el) el.scrollIntoView({block:'nearest'});
+  }
+  clear.onclick = ()=>{ input.value=''; state.q=''; clear.style.display='none'; closeAc(); render(); input.focus(); };
+})();
 document.getElementById('density').oninput = e => { document.documentElement.style.setProperty('--cols', e.target.value); localStorage.setItem('wear_cols', e.target.value); };
 document.getElementById('close').onclick = ()=>document.getElementById('scrim').classList.remove('on');
 document.getElementById('scrim').onclick = e => { if(e.target.id==='scrim') document.getElementById('scrim').classList.remove('on'); };
diff --git a/verification/search-typeahead.png b/verification/search-typeahead.png
new file mode 100644
index 0000000..45e7916
Binary files /dev/null and b/verification/search-typeahead.png differ

← 6ccdfb9 Clear the bucket hat eyelets and update verified preview han  ·  back to CelebritySignatures  ·  wear: add all-over-print signature shirts (camp button-up + 3693ec3 →