@@ -15,7 +15,11 @@ import {
1515 UseComboboxState ,
1616 UseComboboxStateChangeOptions ,
1717} from "downshift" ;
18- import { getStringSlices , JsonSearchEntry } from "~/utilities/search" ;
18+ import {
19+ getComponentSlices ,
20+ getStringSlices ,
21+ JsonSearchEntry ,
22+ } from "~/utilities/search" ;
1923import Fuse from "fuse.js" ;
2024import classnames from "~/utilities/classnames" ;
2125import { iconForValue } from "~/utilities/icons" ;
@@ -226,8 +230,6 @@ export function SearchItem({
226230 const itemValue = heroPath . first ( json ) ;
227231 const ItemIcon = iconForValue ( itemValue ) ;
228232
229- const components = heroPath . components . slice ( 1 ) ;
230-
231233 return (
232234 < li { ...itemProps } className = { classnames ( "w-full hover:cursor-pointer" ) } >
233235 < div
@@ -246,24 +248,11 @@ export function SearchItem({
246248 ) }
247249 > </ ItemIcon >
248250 < div className = "flex flex-col w-full ml-3" >
249- < div className = "path flex items-center gap-1 mb-1 text-slate-800 dark:text-white group-hover:text-white" >
250- { components . map ( ( c , index ) => {
251- return [
252- < Body key = { c . toString ( ) + index + "body" } className = "text-lg" >
253- { c . toString ( ) }
254- </ Body > ,
255- ] . concat (
256- index + 1 === components . length
257- ? [ ]
258- : [
259- < ChevronRightIcon
260- key = { c . toString ( ) + index + "arrow" }
261- className = "w-4 h-4"
262- /> ,
263- ]
264- ) ;
265- } ) }
266- </ div >
251+ < SearchPathResult
252+ path = { heroPath }
253+ searchResult = { result }
254+ isHighlighted = { isHighlighted }
255+ />
267256 < div className = "key-value flex justify-between" >
268257 { result . item . rawValue && (
269258 < SearchResultValue
@@ -290,6 +279,72 @@ export function SearchItem({
290279 ) ;
291280}
292281
282+ // Outputs the following pair for each component except for the last one:
283+ // <Body className="text-lg">{component}</Body>,
284+ // <ChevronRightIcon className="w-4 h-4" />,
285+ //
286+ // Highlights parts of the component that match the search query.
287+ // The match indices match against the stringified version of the path (e.g. $.foo.bar.0.details.description)
288+ //
289+ // If combined component strings are too long, then we need to choose some components to hide behind an ellipsis, making sure we don't hide matches
290+ function SearchPathResult ( {
291+ path,
292+ searchResult,
293+ isHighlighted,
294+ maxWeight = 90 ,
295+ } : {
296+ path : JSONHeroPath ;
297+ isHighlighted : boolean ;
298+ searchResult : Fuse . FuseResult < JsonSearchEntry > ;
299+ maxWeight ?: number ;
300+ } ) {
301+ const components = path . components . slice ( 1 ) ;
302+
303+ const match = ( searchResult . matches ?? [ ] ) . find (
304+ ( match ) => match . key === "path" && match . indices . length > 0
305+ ) ;
306+
307+ const matchingIndices = ( match ?. indices ?? [ ] ) as [ number , number ] [ ] ;
308+
309+ const displayPath = components . join ( "." ) ;
310+
311+ const slices = getComponentSlices (
312+ displayPath ,
313+ matchingIndices . map ( ( [ start , end ] ) => [ start - 2 , end - 2 ] ) ,
314+ maxWeight
315+ ) ;
316+
317+ return (
318+ < >
319+ { slices . map ( ( slice , i ) =>
320+ slice . type === "component" ? (
321+ < span
322+ key = { i }
323+ className = {
324+ slice . slice . isMatch
325+ ? classnames (
326+ "font-sans text-base" ,
327+ isHighlighted
328+ ? "text-white underline underline-offset-1"
329+ : "text-indigo-400"
330+ )
331+ : "font-sans text-base"
332+ }
333+ >
334+ { slice . slice . slice }
335+ </ span >
336+ ) : slice . type === "ellipsis" ? (
337+ < Body key = { i } className = "text-lg mx-1" >
338+ …
339+ </ Body >
340+ ) : (
341+ < ChevronRightIcon key = { i } className = "w-4 h-4 mx-1" />
342+ )
343+ ) }
344+ </ >
345+ ) ;
346+ }
347+
293348function SearchResultValue ( {
294349 isHighlighted,
295350 keyName,
0 commit comments