Components
Loading preview...
Rich text input with @mentions and AI autocomplete. Inline tag chips, ghost text suggestions. Keyboard-first, fully customizable.
@tigerabrodi
npx shadcn@latest add https://21st.dev/r/tigerabrodi/smart-textbox"use client"
import { useState } from "react"
import {
SmartTextbox,
fuzzyFilter,
getPlainText,
} from "@tigerabrodioss/fude"
import type { MentionItem, Segment } from "@tigerabrodioss/fude"
const files: MentionItem[] = [
{ id: "1", searchValue: "use-image-drag.ts", label: "use-image-drag.ts" },
{ id: "2", searchValue: "canvas-renderer.ts", label: "canvas-renderer.ts" },
{ id: "3", searchValue: "viewport.ts", label: "viewport.ts" },
{ id: "4", searchValue: "drag-handler.ts", label: "drag-handler.ts" },
{ id: "5", searchValue: "serializer.ts", label: "serializer.ts" },
{ id: "6", searchValue: "cursor-utils.ts", label: "cursor-utils.ts" },
]
const suggestions = [
"and make it work with the new API",
"to use the updated drag handler",
"and add proper error handling",
]
export default function SmartTextboxDemo() {
const [segments, setSegments] = useState<Segment[]>([])
const [submitted, setSubmitted] = useState<string | null>(null)
return (
<div className="flex flex-col items-center justify-center min-h-[400px] gap-6 p-8 w-full max-w-xl mx-auto">
<div className="w-full space-y-3">
<label className="text-sm font-medium text-neutral-400">
Try typing @ to mention a file, then keep typing for autocomplete
</label>
<SmartTextbox
value={segments}
onChange={setSegments}
onFetchMentions={async (query) => fuzzyFilter(query, files)}
onFetchSuggestions={async () => suggestions}
onSubmit={(segs) => setSubmitted(getPlainText(segs))}
placeholder="Type @ to mention, or start typing..."
classNames={{
root: "w-full",
input:
"w-full rounded-lg border border-neutral-700 bg-neutral-900 px-3 py-2.5 text-sm text-neutral-100 outline-none focus:border-neutral-500 focus:ring-1 focus:ring-neutral-500 transition-colors",
tag: "inline-flex items-center gap-1 rounded-md bg-neutral-800 border border-neutral-600 px-1.5 py-0.5 text-xs font-medium text-neutral-300",
tagHighlighted: "ring-2 ring-blue-500 border-blue-500",
dropdown:
"rounded-lg border border-neutral-700 bg-neutral-900 shadow-xl overflow-hidden",
dropdownItem:
"px-3 py-2 text-sm text-neutral-300 hover:bg-neutral-800 cursor-pointer transition-colors",
ghostText: "text-neutral-600",
}}
/>
</div>
{submitted && (
<div className="w-full rounded-lg border border-neutral-800 bg-neutral-900/50 p-3">
<div className="text-xs text-neutral-500 mb-1">Submitted:</div>
<div className="text-sm text-neutral-300 font-mono">{submitted}</div>
</div>
)}
</div>
)
}