Search Input
唯一的搜索框:左边放大镜,右边清除键,空着时可以显示快捷键,手机键盘出现「搜索」键。
⌘K
输入后右端出现清除键;清除后光标留在框里。
search-input-demo.tsx
"use client"
import { useState } from "react"
import { SearchInput } from "@/components/ui/search-input"
const TITLES = ["药屋少女的呢喃 第二季", "葬送的芙莉莲 第二季", "间谍过家家 第三季", "迷宫饭", "孤独摇滚!"]
export default function SearchInputDemo() {
const [query, setQuery] = useState("")
const hits = TITLES.filter((title) => title.includes(query.trim()))
return (
<div className="flex w-full max-w-sm flex-col gap-2">
<SearchInput
value={query}
onValueChange={setQuery}
placeholder="搜索番剧、字幕组…"
shortcut="⌘K"
aria-label="搜索番剧"
/>
<p className="text-[12px] text-muted-foreground">
{query ? (
<>
找到 <span className="anibt-num">{hits.length}</span> 部:{hits.join("、") || "没有匹配的番剧"}
</>
) : (
"输入后右端出现清除键;清除后光标留在框里。"
)}
</p>
</div>
)
}安装
npx shadcn@latest add @anibt/search-input第一次用之前:在 components.json 里登记 @anibt,再单独运行一次 npx shadcn@latest add @anibt/theme(见快速开始)。只有单独安装主题时,CLI 才会把 shadcn 的标准颜色换成白樱 / 夜樱;随组件带进来的主题只补上 AniBT 自己的变量和配方。 CLI 会一并装好 @anibt/input、@anibt/kbd。
手动安装
- 1. 先装主题组件只读
@anibt/theme的 token 和配方,没有主题就没有样子。npx shadcn@latest add @anibt/theme - 2. 安装依赖
npm install lucide-react - 3. 它还用到
npx shadcn@latest add @anibt/input @anibt/kbd - 4. 把下面的文件放进项目components/ui/search-input.tsx
"use client" import * as React from "react" import { SearchIcon, XIcon } from "lucide-react" import { cn } from "@/lib/utils" import { Input } from "@/components/ui/input" import { Kbd } from "@/components/ui/kbd" /** * SearchInput — the one AniBT 「贴纸手账」 search box. * * An `Input` with the magnifier inside its left edge and a clear key at its * right end. It owns the details every hand-rolled search box gets wrong: * * - `type="search"` plus `enterKeyHint="search"`, so a phone keyboard shows * its search key; the browser's own cancel button is hidden, so the themed * clear key is the only one. * - The clear key (a quiet 24px square, named `clearLabel`) empties the field * and puts the caret back in it, so clearing by keyboard keeps focus. * - `shortcut` shows a key cap (⌘K) while the field is empty — a hint only; * binding the key is the page's job. * * Controlled (`value` + `onValueChange`) or not (`defaultValue`). Every other * prop, `ref` included, reaches the `<input>`. 32 high like every A field; * give it `inputClassName="h-11 md:h-8"` where a phone needs a bigger target. */ type SearchInputProps = Omit<React.ComponentProps<"input">, "type" | "value" | "defaultValue" | "onChange"> & { value?: string defaultValue?: string onValueChange?: (value: string) => void onChange?: React.ChangeEventHandler<HTMLInputElement> /** Classes for the `<input>`; `className` sizes the wrapper. */ inputClassName?: string /** A key cap shown while the field is empty, e.g. "⌘K". */ shortcut?: React.ReactNode /** The clear key's accessible name. */ clearLabel?: string } function SearchInput({ value: valueProp, defaultValue = "", onValueChange, onChange, className, inputClassName, shortcut, clearLabel = "清除", placeholder = "搜索", disabled, readOnly, ref, ...props }: SearchInputProps) { const innerRef = React.useRef<HTMLInputElement>(null) const [uncontrolled, setUncontrolled] = React.useState(defaultValue) const controlled = valueProp !== undefined const value = controlled ? valueProp : uncontrolled const setRefs = React.useCallback( (node: HTMLInputElement | null) => { innerRef.current = node if (typeof ref === "function") return ref(node) if (ref) ref.current = node }, [ref] ) const commit = (next: string) => { if (!controlled) setUncontrolled(next) onValueChange?.(next) } const showClear = value.length > 0 && !disabled && !readOnly return ( <div data-slot="search-input" className={cn("relative w-full min-w-0", className)}> <SearchIcon aria-hidden="true" className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" /> <Input ref={setRefs} type="search" enterKeyHint="search" autoComplete="off" placeholder={placeholder} value={value} disabled={disabled} readOnly={readOnly} onChange={(event) => { onChange?.(event) commit(event.target.value) }} className={cn( "pl-9 [&::-webkit-search-cancel-button]:hidden [&::-webkit-search-decoration]:hidden", showClear || shortcut ? "pr-9" : null, inputClassName )} {...props} /> {showClear ? ( <button type="button" aria-label={clearLabel} title={clearLabel} onClick={() => { commit("") innerRef.current?.focus() }} className="absolute top-1/2 right-1.5 flex size-6 -translate-y-1/2 cursor-pointer items-center justify-center rounded-[6px] text-muted-foreground transition-colors duration-150 hover:bg-accent hover:text-accent-foreground focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-solid" > <XIcon className="size-4" aria-hidden="true" /> </button> ) : shortcut ? ( <Kbd className="absolute top-1/2 right-2 -translate-y-1/2">{shortcut}</Kbd> ) : null} </div> ) } export { SearchInput } export type { SearchInputProps }
用法
用法
import { SearchInput } from "@/components/ui/search-input"
const [query, setQuery] = useState("")
<SearchInput value={query} onValueChange={setQuery} placeholder="搜索番剧、字幕组…" shortcut="⌘K" />它替你做的事
type="search"加enterKeyHint="search":手机键盘出现「搜索」键。浏览器自带的清除叉被隐藏,只留主题里的清除键。- 有内容时右端出现清除键(24px 的安静方块,悬停粉色淡底,读屏念
clearLabel);清除后光标留在框里,用键盘清除也不会丢焦点。 shortcut在框空着时显示一个键帽(⌘K)。它只是提示,绑定快捷键是页面的事。- 受控(
value+onValueChange)或非受控(defaultValue)都行。
状态
search-input-states.tsx
import { SearchInput } from "@/components/ui/search-input"
export default function SearchInputStates() {
return (
<div className="grid w-full max-w-xl gap-3 sm:grid-cols-2">
<SearchInput defaultValue="芙莉莲" aria-label="搜索(有内容)" />
<SearchInput placeholder="搜索字幕组" aria-label="搜索字幕组" />
<SearchInput placeholder="同步中,暂不可搜索" disabled aria-label="搜索(不可用)" />
{/* A phone-sized target: 44 high below md, A's 32 from md. */}
<SearchInput placeholder="手机上更高" inputClassName="h-11 md:h-8" aria-label="搜索(手机加高)" />
</div>
)
}材质和状态与 Input 相同。默认 32 高;手机上需要 44px 的触控区时,给 inputClassName="h-11 md:h-8"。
属性
SearchInput
valuestring默认 —受控的值
defaultValuestring默认 ""非受控的初始值
onValueChange(value: string) => void默认 —输入或清除时调用
shortcutReactNode默认 —框空着时右端的键帽,例如 "⌘K"
placeholderstring默认 "搜索"占位文字
clearLabelstring默认 "清除"清除键的可读名称
classNamestring默认 —外层的类名,管宽度
inputClassNamestring默认 —输入框本身的类名
其余属性(ref、onKeyDown、autoFocus、aria-label……)都交给里面的 <input>。
