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));
}