11import React , { Fragment , useState , useEffect , useMemo , useRef } from 'react' ;
2+ import MiniSearch from 'minisearch' ;
23import { Combobox , Dialog , Transition } from '@headlessui/react' ;
34import { MagnifyingGlassIcon } from '@heroicons/react/20/solid' ;
45import {
@@ -42,6 +43,7 @@ const typeIcons: any = {
4243 AsyncAPI : DocumentTextIcon ,
4344 Design : Square2StackIcon ,
4445 Container : CircleStackIcon ,
46+ Doc : DocumentTextIcon ,
4547 default : DocumentTextIcon ,
4648} ;
4749
@@ -61,6 +63,7 @@ const typeColors: any = {
6163 AsyncAPI : 'text-violet-500 dark:text-violet-400 bg-violet-50 dark:bg-violet-500/10 ring-violet-200 dark:ring-violet-500/30' ,
6264 Design : 'text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-500/10 ring-gray-200 dark:ring-gray-500/30' ,
6365 Container : 'text-indigo-500 dark:text-indigo-400 bg-indigo-50 dark:bg-indigo-500/10 ring-indigo-200 dark:ring-indigo-500/30' ,
66+ Doc : 'text-slate-500 dark:text-slate-400 bg-slate-50 dark:bg-slate-500/10 ring-slate-200 dark:ring-slate-500/30' ,
6467 default : 'text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-500/10 ring-gray-200 dark:ring-gray-500/30' ,
6568} ;
6669
@@ -118,6 +121,17 @@ interface SearchIndexPayload {
118121 items ?: SearchNode [ ] ;
119122}
120123
124+ interface DocsSearchIndexItemCompact {
125+ i : string ;
126+ t : string ;
127+ u : string ;
128+ c : string ;
129+ }
130+
131+ interface DocsSearchIndexPayload {
132+ i ?: DocsSearchIndexItemCompact [ ] ;
133+ }
134+
121135const normalizeSearchIndexPayload = ( payload : SearchIndexPayload ) : SearchNode [ ] => {
122136 if ( payload . i ) {
123137 return payload . i . map ( ( item ) => ( {
@@ -137,7 +151,9 @@ export default function SearchModal() {
137151 const [ open , setOpen ] = useState ( false ) ;
138152 const [ activeFilter , setActiveFilter ] = useState ( 'all' ) ;
139153 const [ searchNodes , setSearchNodes ] = useState < SearchNode [ ] > ( [ ] ) ;
154+ const [ docsIndexItems , setDocsIndexItems ] = useState < DocsSearchIndexItemCompact [ ] > ( [ ] ) ;
140155 const [ isLoadingSearchIndex , setIsLoadingSearchIndex ] = useState ( false ) ;
156+ const [ isLoadingDocsIndex , setIsLoadingDocsIndex ] = useState ( false ) ;
141157 const [ searchIndexLoadError , setSearchIndexLoadError ] = useState < string | null > ( null ) ;
142158 const favorites = useStore ( favoritesStore ) ;
143159 const inputRef = useRef < HTMLInputElement > ( null ) ;
@@ -185,6 +201,33 @@ export default function SearchModal() {
185201 } ) ;
186202 } , [ open , searchNodes . length , isLoadingSearchIndex ] ) ;
187203
204+ useEffect ( ( ) => {
205+ if ( ! open || query . trim ( ) . length < 2 || docsIndexItems . length > 0 || isLoadingDocsIndex ) {
206+ return ;
207+ }
208+
209+ setIsLoadingDocsIndex ( true ) ;
210+
211+ const apiUrl = buildUrl ( '/api/search-docs-index.json' , true ) ;
212+
213+ fetch ( apiUrl )
214+ . then ( ( response ) => {
215+ if ( ! response . ok ) {
216+ throw new Error ( `Failed to fetch docs search index: ${ response . status } ` ) ;
217+ }
218+ return response . json ( ) as Promise < DocsSearchIndexPayload > ;
219+ } )
220+ . then ( ( payload ) => {
221+ setDocsIndexItems ( payload . i || [ ] ) ;
222+ } )
223+ . catch ( ( ) => {
224+ // non-blocking for regular search experience
225+ } )
226+ . finally ( ( ) => {
227+ setIsLoadingDocsIndex ( false ) ;
228+ } ) ;
229+ } , [ open , query , docsIndexItems . length , isLoadingDocsIndex ] ) ;
230+
188231 const closeModal = ( ) => {
189232 if ( ( window as any ) . searchModalState ) {
190233 ( window as any ) . searchModalState . close ( ) ;
@@ -295,6 +338,28 @@ export default function SearchModal() {
295338 return new Map ( searchNodes . map ( ( node ) => [ node . key , node ] ) ) ;
296339 } , [ searchNodes ] ) ;
297340
341+ const docsMiniSearch = useMemo ( ( ) => {
342+ if ( docsIndexItems . length === 0 ) return null ;
343+
344+ const index = new MiniSearch < DocsSearchIndexItemCompact > ( {
345+ fields : [ 't' , 'c' ] ,
346+ storeFields : [ 'i' , 't' , 'u' , 'c' ] ,
347+ searchOptions : {
348+ fuzzy : 0.2 ,
349+ prefix : true ,
350+ } ,
351+ } ) ;
352+
353+ index . addAll ( docsIndexItems ) ;
354+ return index ;
355+ } , [ docsIndexItems ] ) ;
356+
357+ const docsSearchResults = useMemo < DocsSearchIndexItemCompact [ ] > ( ( ) => {
358+ if ( ! docsMiniSearch || query . trim ( ) . length < 2 ) return [ ] ;
359+
360+ return docsMiniSearch . search ( query , { fuzzy : 0.2 , prefix : true , boost : { t : 3 , c : 1 } } ) . slice ( 0 , 20 ) as any ;
361+ } , [ docsMiniSearch , query ] ) ;
362+
298363 const filteredItems = useMemo ( ( ) => {
299364 if ( query === '' ) {
300365 // Show favorites when search is empty
@@ -324,6 +389,17 @@ export default function SearchModal() {
324389 // Start with searchable items (already filtered by query)
325390 let result = searchableItems ;
326391
392+ const docsItems = docsSearchResults . map ( ( doc : DocsSearchIndexItemCompact ) => ( {
393+ id : `doc:${ doc . i } ` ,
394+ name : doc . t ,
395+ url : buildUrl ( doc . u ) ,
396+ type : 'Doc' ,
397+ key : `doc:${ doc . i } ` ,
398+ rawNode : {
399+ summary : doc . c . slice ( 0 , 120 ) ,
400+ } ,
401+ } ) ) ;
402+
327403 // Apply type filter
328404 if ( activeFilter !== 'all' ) {
329405 if ( activeFilter === 'Message' ) {
@@ -335,8 +411,10 @@ export default function SearchModal() {
335411 }
336412 }
337413
338- return result . slice ( 0 , 50 ) ; // Limit results for performance
339- } , [ searchableItems , query , activeFilter , favorites , searchNodeLookup ] ) ;
414+ const merged = activeFilter === 'all' ? [ ...result , ...docsItems ] : result ;
415+
416+ return merged . slice ( 0 , 50 ) ; // Limit results for performance
417+ } , [ searchableItems , query , activeFilter , favorites , searchNodeLookup , docsSearchResults ] ) ;
340418
341419 return (
342420 < Transition . Root
0 commit comments