import { esc } from '../utils.js'; import { createElement, Bird, Bone, Bug, Cat, Dog, Fish, Panda, PawPrint, Rabbit, Rat, Shell, Snail, Shrimp, Squirrel, Turtle, Worm, Cherry, Star, Moon, Sprout, } from 'lucide'; const ENDPOINT = 'https://typeahead.waow.tech/xrpc/app.bsky.actor.searchActorsTypeahead'; const PLACEHOLDER_ICONS = [ Bird, Bone, Bug, Cat, Dog, Fish, Panda, PawPrint, Rabbit, Rat, Shell, Snail, Shrimp, Squirrel, Turtle, Worm, Cherry, Star, Moon, Sprout, ]; function hashStr(s) { let h = 0; for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0; return Math.abs(h); } function placeholderIconHtml(key) { const icon = PLACEHOLDER_ICONS[hashStr(key) % PLACEHOLDER_ICONS.length]; return createElement(icon, { width: 14, height: 14, 'stroke-width': 2 }).outerHTML; } export function attachTypeahead(input, list, { onSubmit }) { let actors = []; let cursor = -1; let seq = 0; const close = () => { list.style.display = 'none'; list.innerHTML = ''; actors = []; cursor = -1; }; const render = () => { if (!actors.length) return close(); list.style.display = 'block'; list.innerHTML = actors.map((a, i) => `
  • ${a.avatar ? `` : placeholderIconHtml(a.did || a.handle)} ${a.displayName ? `${esc(a.displayName)}` : ''} @${esc(a.handle)}
  • `).join(''); }; const select = (i) => { const a = actors[i]; if (!a) return; input.value = a.handle; close(); }; const fetchResults = async (q) => { const mySeq = ++seq; try { const res = await fetch(`${ENDPOINT}?q=${encodeURIComponent(q)}&limit=6`, { headers: { 'X-Client': 'followtunnel' }, }); if (mySeq !== seq) return; const data = await res.json(); actors = data.actors || []; cursor = -1; render(); } catch { if (mySeq !== seq) return; close(); } }; let timer; input.addEventListener('input', () => { const q = input.value.trim().replace(/^@/, ''); clearTimeout(timer); if (q.length < 2) return close(); timer = setTimeout(() => fetchResults(q), 200); }); input.addEventListener('keydown', e => { if (list.style.display === 'none' || !actors.length) { if (e.key === 'Enter') onSubmit(input.value.trim()); return; } if (e.key === 'ArrowDown') { e.preventDefault(); cursor = (cursor + 1) % actors.length; render(); } else if (e.key === 'ArrowUp') { e.preventDefault(); cursor = (cursor - 1 + actors.length) % actors.length; render(); } else if (e.key === 'Enter') { e.preventDefault(); if (cursor >= 0) { select(cursor); onSubmit(input.value.trim()); } else { onSubmit(input.value.trim()); } } else if (e.key === 'Escape') { close(); } }); list.addEventListener('mousedown', e => { const li = e.target.closest('li[data-i]'); if (!li) return; e.preventDefault(); select(Number(li.dataset.i)); }); input.addEventListener('blur', () => setTimeout(close, 150)); }