{"version":3,"sources":["../../src/docs/DocsSearch.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport Link from 'next/link';\nimport type { SearchEntry } from './types';\n\ninterface DocsSearchProps {\n  index: SearchEntry[];\n  /** Placeholder; defaults to \"Search docs\". */\n  placeholder?: string;\n  /** Hits-per-query cap; defaults to 8. */\n  limit?: number;\n}\n\ninterface Hit {\n  entry: SearchEntry;\n  score: number;\n  snippet: string;\n}\n\n/**\n * Lightweight client-side fuzzy search. Tokens in the query must all\n * appear (case-insensitive) somewhere in the entry's title or body.\n * Title matches score higher than body matches. The first body match\n * forms the snippet shown in the dropdown.\n */\nfunction search(index: SearchEntry[], query: string, limit: number): Hit[] {\n  const q = query.trim().toLowerCase();\n  if (!q) return [];\n  const tokens = q.split(/\\s+/).filter((t) => t.length > 1);\n  if (tokens.length === 0) return [];\n\n  const hits: Hit[] = [];\n  for (const entry of index) {\n    const title = entry.title.toLowerCase();\n    const body = entry.body.toLowerCase();\n    let score = 0;\n    let firstBodyHit = -1;\n\n    for (const tok of tokens) {\n      const titleHits = countOccurrences(title, tok);\n      const bodyHits = countOccurrences(body, tok);\n      if (titleHits === 0 && bodyHits === 0) {\n        score = -1;\n        break;\n      }\n      score += titleHits * 10 + bodyHits;\n      if (bodyHits > 0 && firstBodyHit < 0) {\n        firstBodyHit = body.indexOf(tok);\n      }\n    }\n    if (score <= 0) continue;\n\n    if (title.includes(q)) score += 50;\n\n    hits.push({ entry, score, snippet: makeSnippet(entry.body, firstBodyHit) });\n  }\n\n  hits.sort((a, b) => b.score - a.score);\n  return hits.slice(0, limit);\n}\n\nfunction countOccurrences(haystack: string, needle: string): number {\n  let count = 0;\n  let i = 0;\n  while ((i = haystack.indexOf(needle, i)) !== -1) {\n    count++;\n    i += needle.length;\n  }\n  return count;\n}\n\nfunction makeSnippet(body: string, hitAt: number): string {\n  if (hitAt < 0) return body.slice(0, 140).trim() + '…';\n  const start = Math.max(0, hitAt - 50);\n  const end = Math.min(body.length, hitAt + 140);\n  const prefix = start === 0 ? '' : '…';\n  const suffix = end === body.length ? '' : '…';\n  return prefix + body.slice(start, end).trim() + suffix;\n}\n\nexport function DocsSearch({ index, placeholder = 'Search docs', limit = 8 }: DocsSearchProps) {\n  const [query, setQuery] = useState('');\n  const [open, setOpen] = useState(false);\n  const [activeIndex, setActiveIndex] = useState(0);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const inputRef = useRef<HTMLInputElement>(null);\n\n  const hits = useMemo(() => search(index, query, limit), [index, query, limit]);\n\n  // ⌘K / Ctrl+K focuses the search.\n  useEffect(() => {\n    function onKeyDown(e: KeyboardEvent) {\n      if ((e.metaKey || e.ctrlKey) && e.key === 'k') {\n        e.preventDefault();\n        inputRef.current?.focus();\n        inputRef.current?.select();\n      }\n      if (e.key === 'Escape') {\n        setOpen(false);\n        inputRef.current?.blur();\n      }\n    }\n    document.addEventListener('keydown', onKeyDown);\n    return () => document.removeEventListener('keydown', onKeyDown);\n  }, []);\n\n  useEffect(() => {\n    function onClick(e: MouseEvent) {\n      if (containerRef.current && !containerRef.current.contains(e.target as Node)) {\n        setOpen(false);\n      }\n    }\n    document.addEventListener('mousedown', onClick);\n    return () => document.removeEventListener('mousedown', onClick);\n  }, []);\n\n  useEffect(() => setActiveIndex(0), [query]);\n\n  function onArrow(e: React.KeyboardEvent<HTMLInputElement>) {\n    if (!open || hits.length === 0) return;\n    if (e.key === 'ArrowDown') {\n      e.preventDefault();\n      setActiveIndex((i) => Math.min(i + 1, hits.length - 1));\n    } else if (e.key === 'ArrowUp') {\n      e.preventDefault();\n      setActiveIndex((i) => Math.max(i - 1, 0));\n    } else if (e.key === 'Enter') {\n      e.preventDefault();\n      const hit = hits[activeIndex];\n      if (hit) window.location.href = hit.entry.href;\n    }\n  }\n\n  return (\n    <div className=\"relative w-full max-w-md\" ref={containerRef}>\n      <div className=\"relative\">\n        <input\n          ref={inputRef}\n          type=\"search\"\n          value={query}\n          onChange={(e) => {\n            setQuery(e.target.value);\n            setOpen(true);\n          }}\n          onFocus={() => setOpen(true)}\n          onKeyDown={onArrow}\n          placeholder={placeholder}\n          className=\"w-full rounded-md border border-border bg-background px-3 py-2 pr-12 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring\"\n          aria-label=\"Search documentation\"\n          aria-autocomplete=\"list\"\n          aria-expanded={open}\n        />\n        <kbd className=\"pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 rounded border border-border bg-muted px-1.5 py-0.5 text-[10px] font-mono text-muted-foreground\">\n          ⌘K\n        </kbd>\n      </div>\n\n      {open && query.trim().length > 1 && (\n        <div className=\"absolute left-0 right-0 z-50 mt-1 max-h-[60vh] overflow-y-auto rounded-md border border-border bg-background shadow-lg\">\n          {hits.length === 0 ? (\n            <div className=\"px-4 py-3 text-sm text-muted-foreground\">\n              No results for &ldquo;{query}&rdquo;\n            </div>\n          ) : (\n            <ul className=\"py-1\" role=\"listbox\">\n              {hits.map((hit, i) => (\n                <li key={hit.entry.href} role=\"option\" aria-selected={i === activeIndex}>\n                  <Link\n                    href={hit.entry.href}\n                    onClick={() => setOpen(false)}\n                    onMouseEnter={() => setActiveIndex(i)}\n                    className={\n                      'block px-4 py-2 text-sm ' +\n                      (i === activeIndex ? 'bg-muted' : 'hover:bg-muted/60')\n                    }\n                  >\n                    <div className=\"flex items-center gap-2\">\n                      <span className=\"font-medium text-foreground\">{hit.entry.title}</span>\n                      <span className=\"font-mono text-[10px] uppercase tracking-wide text-muted-foreground\">\n                        {hit.entry.group}\n                      </span>\n                    </div>\n                    <div className=\"mt-0.5 line-clamp-2 text-xs text-muted-foreground\">\n                      {hit.snippet}\n                    </div>\n                  </Link>\n                </li>\n              ))}\n            </ul>\n          )}\n        </div>\n      )}\n    </div>\n  );\n}\n"],"mappings":"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","names":["Link"]}