The trick with the matches is that I do not just find them, I wrap them. A small function walks the text and splits it around each hit, so the match can be its own styled piece rather than a highlight painted over the row. That is why it works the same in a name, a domain, or a comma list.
// Split the text around each hit and mark the matches.
function highlight(text, q) {
const out = []
const lower = text.toLowerCase()
let i = 0
for (let at; (at = lower.indexOf(q, i)) !== -1; i = at + q.length) {
out.push(text.slice(i, at))
out.push(<mark className="text-amber-500">{text.slice(at, at + q.length)}</mark>)
}
out.push(text.slice(i))
return out
}
Searching a person is relational, not literal. Every file and list carries the people on it, so looking up a name pulls in the things they touch, not only the rows their name shows up in. Resting and results are two separate layouts too, swapped on whether the box is empty, so I could tune each one without the other getting in the way.