feat : add fuzzy matching for searching
CI / Verify (push) Failing after 58s
CI / Build immutable images and deploy (push) Has been skipped

This commit is contained in:
2026-09-12 16:07:16 +07:00 Unverified
parent 0ec214d3e0
commit 92b57e0ed3
15 changed files with 318 additions and 79 deletions
+33 -1
View File
@@ -4,6 +4,8 @@ import * as React from "react"
import { Combobox as ComboboxPrimitive } from "@base-ui/react"
import { cn } from "@/lib/utils"
import { searchItems } from "@/lib/search"
import { SearchNotice } from "@/components/search-notice"
import { Button } from "@/components/ui/button"
import {
InputGroup,
@@ -14,6 +16,30 @@ import {
import { ChevronDownIcon, XIcon, CheckIcon } from "lucide-react"
const Combobox = ComboboxPrimitive.Root
const FuzzyContext = React.createContext(false)
function FuzzyCombobox<Value, Multiple extends boolean | undefined = false>({
items = [], searchNames, searchText, children, onInputValueChange, ...props
}: ComboboxPrimitive.Root.Props<Value, Multiple> & {
items: Value[];
searchNames: (item: Value) => readonly string[];
searchText?: (item: Value) => string;
}) {
const [query, setQuery] = React.useState("")
const result = React.useMemo(() => searchItems(items, query,
searchText ?? ((item) => searchNames(item).join(" ")), searchNames),
[items, query, searchNames, searchText])
return (
<FuzzyContext.Provider value={result.fuzzy}>
<ComboboxPrimitive.Root {...props} items={result.items} filter={null}
onInputValueChange={(value, details) => {
setQuery(details.reason === "input-change" ? value : "")
onInputValueChange?.(value, details)
}}
>{children}</ComboboxPrimitive.Root>
</FuzzyContext.Provider>
)
}
function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {
return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />
@@ -85,6 +111,7 @@ function ComboboxInput({
}
function ComboboxContent({
children,
className,
side = "bottom",
sideOffset = 6,
@@ -97,6 +124,7 @@ function ComboboxContent({
ComboboxPrimitive.Positioner.Props,
"side" | "align" | "sideOffset" | "alignOffset" | "anchor"
>) {
const fuzzy = React.useContext(FuzzyContext)
return (
<ComboboxPrimitive.Portal>
<ComboboxPrimitive.Positioner
@@ -112,7 +140,10 @@ function ComboboxContent({
data-chips={!!anchor}
className={cn("group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) overflow-hidden rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[chips=true]:min-w-(--anchor-width) data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:border-input/30 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:shadow-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
{...props}
/>
>
<SearchNotice fuzzy={fuzzy} />
{children}
</ComboboxPrimitive.Popup>
</ComboboxPrimitive.Positioner>
</ComboboxPrimitive.Portal>
)
@@ -278,6 +309,7 @@ function useComboboxAnchor() {
}
export {
FuzzyCombobox,
Combobox,
ComboboxInput,
ComboboxContent,