The trick with the matches is that they are not just found, they are wrapped. 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 each can be tuned without the other getting in the way.