AniBTUI
✦ 组件@anibt/search-inputregistry JSON

Search Input

唯一的搜索框:左边放大镜,右边清除键,空着时可以显示快捷键,手机键盘出现「搜索」键。

⌘K

输入后右端出现清除键;清除后光标留在框里。

安装

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. 1. 先装主题组件只读 @anibt/theme 的 token 和配方,没有主题就没有样子。
    npx shadcn@latest add @anibt/theme
  2. 2. 安装依赖
    npm install lucide-react
  3. 3. 它还用到
    npx shadcn@latest add @anibt/input @anibt/kbd
  4. 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)都行。

状态

材质和状态与 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>。