feat : remove notice search text [no ci]

This commit is contained in:
2026-09-12 16:15:35 +07:00 Unverified
parent e3c686caed
commit 28a59a8934
9 changed files with 11 additions and 25 deletions
+2 -2
View File
@@ -14,11 +14,11 @@ Use this project’s shared search implementation for finding entities by name o
- Restrict fuzzy candidates to names, aliases, and individual whitespace-separated words. Keep overview prose and internal keys available for existing direct searches, but exclude them from fuzzy candidates. - Restrict fuzzy candidates to names, aliases, and individual whitespace-separated words. Keep overview prose and internal keys available for existing direct searches, but exclude them from fuzzy candidates.
- Normalize with NFC, trim, lowercase, and compare Unicode code points. Keep queries shorter than four code points strict. - Normalize with NFC, trim, lowercase, and compare Unicode code points. Keep queries shorter than four code points strict.
- Use bounded optimal-string-alignment Damerau–Levenshtein distance, including adjacent transpositions. Allow one edit for 4–7 code points and two for longer queries, capped at 25% of query length. Rank by edit distance with stable source-order ties. - Use bounded optimal-string-alignment Damerau–Levenshtein distance, including adjacent transpositions. Allow one edit for 4–7 code points and two for longer queries, capped at 25% of query length. Rank by edit distance with stable source-order ties.
- Empty queries return the eligible collection. Show the localized close-match notice only when fuzzy fallback returns results. - Empty queries return the eligible collection. Display fuzzy results without an explanatory message.
## Reuse the project implementation ## Reuse the project implementation
Use `searchItems` from `lib/search.ts`, returning `{ items, fuzzy }`. Supply separate selectors for existing direct-search text and fuzzy candidate names. Use `SearchNotice` from `components/search-notice.tsx` for fallback feedback. Use `searchItems` from `lib/search.ts`, returning `{ items, fuzzy }`. Supply separate selectors for existing direct-search text and fuzzy candidate names.
Use `searchTeams` from `lib/guides/team-search.ts` for collection-level team searches. Apply active groups, permissions, and category restrictions before matching. Explicit DPS sorting takes precedence over fuzzy ranking. Direct team queries must match within one candidate name, not span multiple members. Use `searchTeams` from `lib/guides/team-search.ts` for collection-level team searches. Apply active groups, permissions, and category restrictions before matching. Explicit DPS sorting takes precedence over fuzzy ranking. Direct team queries must match within one candidate name, not span multiple members.
+1 -2
View File
@@ -1,7 +1,6 @@
"use client"; "use client";
import { searchItems } from "@/lib/search"; import { searchItems } from "@/lib/search";
import { SearchNotice } from "@/components/search-notice";
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
@@ -80,7 +79,7 @@ export function CreateGuideFlow({ characters, existingKeys }: { characters: Char
<InputGroupAddon><SearchIcon /></InputGroupAddon> <InputGroupAddon><SearchIcon /></InputGroupAddon>
<InputGroupInput value={query} onChange={(event) => setQuery(event.target.value)} placeholder="ค้นหาตัวละคร…" aria-label="ค้นหาตัวละคร" /> <InputGroupInput value={query} onChange={(event) => setQuery(event.target.value)} placeholder="ค้นหาตัวละคร…" aria-label="ค้นหาตัวละคร" />
</InputGroup> </InputGroup>
<SearchNotice fuzzy={search.fuzzy} />
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4"> <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
{filtered.map((character) => { {filtered.map((character) => {
const disabled = existing.has(character.key); const disabled = existing.has(character.key);
+1 -2
View File
@@ -1,7 +1,6 @@
"use client"; "use client";
import { searchItems } from "@/lib/search"; import { searchItems } from "@/lib/search";
import { SearchNotice } from "@/components/search-notice";
import Image from "next/image"; import Image from "next/image";
import { SearchIcon, Trash2Icon } from "lucide-react"; import { SearchIcon, Trash2Icon } from "lucide-react";
@@ -146,7 +145,7 @@ export function GlossaryManager({
<Input id="glossary-search" className="pl-9" value={query} onChange={(event) => setQuery(event.target.value)} /> <Input id="glossary-search" className="pl-9" value={query} onChange={(event) => setQuery(event.target.value)} />
</div> </div>
</Field> </Field>
<SearchNotice fuzzy={search.fuzzy} />
{visibleAliases.map((entry) => { {visibleAliases.map((entry) => {
const name = itemNames.get(`${entry.kind}:${entry.catalogKey}`); const name = itemNames.get(`${entry.kind}:${entry.catalogKey}`);
return ( return (
+3 -4
View File
@@ -1,7 +1,6 @@
"use client"; "use client";
import { searchItems } from "@/lib/search"; import { searchItems } from "@/lib/search";
import { SearchNotice } from "@/components/search-notice";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import Image from "next/image"; import Image from "next/image";
@@ -3340,7 +3339,7 @@ function TeamEditor({
<FieldGroup className="mt-4"> <FieldGroup className="mt-4">
<Field> <Field>
<FieldLabel htmlFor="team-search">ค้นหา Team</FieldLabel> <FieldLabel htmlFor="team-search">ค้นหา Team</FieldLabel>
<SearchNotice fuzzy={teamSearch.fuzzy} />
<div className="relative"> <div className="relative">
<SearchIcon <SearchIcon
aria-hidden="true" aria-hidden="true"
@@ -4094,7 +4093,7 @@ function TeamEditor({
<ToggleGroupItem value="artifact">อาร์ติแฟกต์</ToggleGroupItem> <ToggleGroupItem value="artifact">อาร์ติแฟกต์</ToggleGroupItem>
</ToggleGroup> </ToggleGroup>
<div className="min-h-0 overflow-y-auto pr-1"> <div className="min-h-0 overflow-y-auto pr-1">
<SearchNotice fuzzy={importTeamSearch.fuzzy} />
{filteredImportTeams.length ? ( {filteredImportTeams.length ? (
<div className="grid gap-4 lg:grid-cols-2"> <div className="grid gap-4 lg:grid-cols-2">
{filteredImportTeams.map((team) => { {filteredImportTeams.map((team) => {
@@ -4203,7 +4202,7 @@ function TeamEditor({
/> />
</div> </div>
<div className="min-h-0 overflow-y-auto pr-1"> <div className="min-h-0 overflow-y-auto pr-1">
<SearchNotice fuzzy={importGuideSearch.fuzzy} />
{filteredImportGuides.length ? ( {filteredImportGuides.length ? (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{filteredImportGuides.map((sourceGuide) => ( {filteredImportGuides.map((sourceGuide) => (
+1 -2
View File
@@ -1,7 +1,6 @@
"use client"; "use client";
import { searchItems } from "@/lib/search"; import { searchItems } from "@/lib/search";
import { SearchNotice } from "@/components/search-notice";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
@@ -213,7 +212,7 @@ export function StructuredDashboard({
/> />
</InputGroup> </InputGroup>
) : null} ) : null}
<SearchNotice fuzzy={search.fuzzy} />
{filteredGuides.length ? ( {filteredGuides.length ? (
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-3"> <div className="grid gap-5 md:grid-cols-2 xl:grid-cols-3">
{filteredGuides.map((guide) => ( {filteredGuides.map((guide) => (
+1 -2
View File
@@ -1,7 +1,6 @@
"use client"; "use client";
import { searchItems } from "@/lib/search"; import { searchItems } from "@/lib/search";
import { SearchNotice } from "@/components/search-notice";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
@@ -59,7 +58,7 @@ export function StructuredDirectory({ guides }: { guides: GuideCard[] }) {
aria-label="ค้นหาไกด์" aria-label="ค้นหาไกด์"
/> />
</InputGroup> </InputGroup>
<SearchNotice fuzzy={search.fuzzy} />
{filtered.length ? ( {filtered.length ? (
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
{filtered.map((guide, index) => ( {filtered.map((guide, index) => (
+1 -2
View File
@@ -1,6 +1,5 @@
"use client"; "use client";
import { SearchNotice } from "@/components/search-notice";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
@@ -395,7 +394,7 @@ export function TeamSearch({ groups }: { groups: PublicGuide["teamGroups"] }) {
return ( return (
<div className="flex min-w-0 flex-col gap-4"> <div className="flex min-w-0 flex-col gap-4">
<SearchNotice fuzzy={search.fuzzy} />
{groups.length > 1 ? ( {groups.length > 1 ? (
<ScrollableNav ariaLabel="เลือกเงื่อนไขทีม" className="w-full min-w-0"> <ScrollableNav ariaLabel="เลือกเงื่อนไขทีม" className="w-full min-w-0">
<ToggleGroup aria-label="เลือกเงื่อนไขทีม" variant="outline" value={[activeCondition]} onValueChange={(values) => { <ToggleGroup aria-label="เลือกเงื่อนไขทีม" variant="outline" value={[activeCondition]} onValueChange={(values) => {
-3
View File
@@ -1,3 +0,0 @@
export function SearchNotice({ fuzzy }: { fuzzy: boolean }) {
return fuzzy ? <p role="status" className="px-2 py-1 text-sm text-muted-foreground">แสดงผลลัพธ์ที่ใกล้เคียง</p> : null;
}
+1 -6
View File
@@ -5,7 +5,6 @@ import { Combobox as ComboboxPrimitive } from "@base-ui/react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { searchItems } from "@/lib/search" import { searchItems } from "@/lib/search"
import { SearchNotice } from "@/components/search-notice"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { import {
InputGroup, InputGroup,
@@ -16,7 +15,6 @@ import {
import { ChevronDownIcon, XIcon, CheckIcon } from "lucide-react" import { ChevronDownIcon, XIcon, CheckIcon } from "lucide-react"
const Combobox = ComboboxPrimitive.Root const Combobox = ComboboxPrimitive.Root
const FuzzyContext = React.createContext(false)
function FuzzyCombobox<Value, Multiple extends boolean | undefined = false>({ function FuzzyCombobox<Value, Multiple extends boolean | undefined = false>({
items = [], searchNames, searchText, children, onInputValueChange, ...props items = [], searchNames, searchText, children, onInputValueChange, ...props
@@ -30,14 +28,12 @@ function FuzzyCombobox<Value, Multiple extends boolean | undefined = false>({
searchText ?? ((item) => searchNames(item).join(" ")), searchNames), searchText ?? ((item) => searchNames(item).join(" ")), searchNames),
[items, query, searchNames, searchText]) [items, query, searchNames, searchText])
return ( return (
<FuzzyContext.Provider value={result.fuzzy}>
<ComboboxPrimitive.Root {...props} items={result.items} filter={null} <ComboboxPrimitive.Root {...props} items={result.items} filter={null}
onInputValueChange={(value, details) => { onInputValueChange={(value, details) => {
setQuery(details.reason === "input-change" ? value : "") setQuery(details.reason === "input-change" ? value : "")
onInputValueChange?.(value, details) onInputValueChange?.(value, details)
}} }}
>{children}</ComboboxPrimitive.Root> >{children}</ComboboxPrimitive.Root>
</FuzzyContext.Provider>
) )
} }
@@ -124,7 +120,6 @@ function ComboboxContent({
ComboboxPrimitive.Positioner.Props, ComboboxPrimitive.Positioner.Props,
"side" | "align" | "sideOffset" | "alignOffset" | "anchor" "side" | "align" | "sideOffset" | "alignOffset" | "anchor"
>) { >) {
const fuzzy = React.useContext(FuzzyContext)
return ( return (
<ComboboxPrimitive.Portal> <ComboboxPrimitive.Portal>
<ComboboxPrimitive.Positioner <ComboboxPrimitive.Positioner
@@ -141,7 +136,7 @@ function ComboboxContent({
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 )} 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} {...props}
> >
<SearchNotice fuzzy={fuzzy} />
{children} {children}
</ComboboxPrimitive.Popup> </ComboboxPrimitive.Popup>
</ComboboxPrimitive.Positioner> </ComboboxPrimitive.Positioner>