+
+
+
+ setQuery(e.target.value)}
+ onKeyDown={onKeyDown}
+ placeholder="Find"
+ className="w-52 bg-transparent text-sm text-[var(--color-text-primary)] outline-none placeholder:text-[var(--color-text-tertiary)]"
+ />
+
+ {count > 0 ? `${activeIndex + 1} / ${count}` : (debouncedQuery.trim() ? '0' : '')}
+
+
+
+
+
+
,
+ document.body,
+ )
+}
+
+// ---- search core (module scope, no React state) ----
+
+/** Walk visible text nodes outside skipped subtrees; return a Range per case-insensitive match. */
+function collectRanges(q: string): Range[] {
+ const ranges: Range[] = []
+ const needle = q.toLowerCase()
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
+ acceptNode(node) {
+ const parent = node.parentElement
+ if (!parent || !node.nodeValue || !node.nodeValue.trim()) return NodeFilter.FILTER_REJECT
+ if (parent.closest(SKIP_CLOSEST)) return NodeFilter.FILTER_REJECT
+ return NodeFilter.FILTER_ACCEPT
+ },
+ })
+ let textNode = walker.nextNode() as Text | null
+ while (textNode) {
+ const text = textNode.nodeValue!.toLowerCase()
+ let idx = text.indexOf(needle)
+ while (idx !== -1) {
+ const range = document.createRange()
+ range.setStart(textNode, idx)
+ range.setEnd(textNode, idx + needle.length)
+ ranges.push(range)
+ idx = text.indexOf(needle, idx + needle.length)
+ }
+ textNode = walker.nextNode() as Text | null
+ }
+ return ranges
+}
+
+/** Register CSS highlights for all matches + the active one, and scroll the active into view. */
+function paint(ranges: Range[], activeIndex: number) {
+ const highlights = (CSS as any).highlights as Map