Components
SmartTextarea
Multiline ghost-text completion with mirror-div positioning and optional auto-resize.
Multiline variant of SmartTextbox. Uses a mirror <div> for accurate ghost positioning across wraps. Optional autoResize grows the textarea to fit content.
Preview
import { useState } from 'react';import { SmartProvider, SmartTextarea } from '@extedcoud/smart-components';import { createMockClient } from '@extedcoud/smart-components/adapters/mock';const client = createMockClient({complete: async (req) => '\n\nLooking forward to next steps.',});export function Example() {const [value, setValue] = useState('');return ( <SmartProvider client={client}> <SmartTextarea value={value} onChange={setValue} placeholder="Compose your email…" autoResize rows={4} /> </SmartProvider>);}Styled example
An email-composer chrome (window header, to/subject metadata, footer status) with a soft violet italic ghost.
New message
✦ AI assistToteam@yourco.comSubjectQuick update
0 chars→ to accept ghost
'use client';import { useState } from 'react';import { SmartProvider, SmartTextarea } from '@extedcoud/smart-components';import { makeGhostMock } from '@/lib/mock-client';const client = makeGhostMock();export default function SmartTextareaStylized() { const [value, setValue] = useState(''); return ( <SmartProvider client={client}> <div className="w-full max-w-xl"> <div className="overflow-hidden rounded-2xl border border-fd-border bg-fd-card shadow-sm"> <div className="flex items-center justify-between border-b border-fd-border bg-fd-secondary/40 px-4 py-2"> <div className="flex items-center gap-2 text-xs text-fd-muted-foreground"> <span className="flex h-2 w-2 rounded-full bg-rose-500/80" /> <span className="flex h-2 w-2 rounded-full bg-amber-500/80" /> <span className="flex h-2 w-2 rounded-full bg-emerald-500/80" /> <span className="ml-2 font-medium">New message</span> </div> <span className="rounded-full bg-violet-500/10 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wider text-violet-600 dark:text-violet-300"> ✦ AI assist </span> </div> <div className="px-4 pt-3"> <div className="grid grid-cols-[60px_1fr] gap-2 text-sm"> <span className="self-center text-fd-muted-foreground">To</span> <span className="self-center text-fd-foreground">team@yourco.com</span> <span className="self-center text-fd-muted-foreground">Subject</span> <span className="self-center text-fd-foreground">Quick update</span> </div> <hr className="my-3 border-fd-border" /> <SmartTextarea value={value} onChange={setValue} placeholder="Hi team, just wanted to say…" autoResize rows={4} className="w-full resize-none bg-transparent text-base text-fd-foreground outline-none" style={{ fontSize: 16 }} ghostStyle={{ color: '#7c3aed', opacity: 0.55, fontStyle: 'italic' }} /> </div> <div className="flex items-center justify-between border-t border-fd-border bg-fd-secondary/30 px-4 py-2 text-xs text-fd-muted-foreground"> <span>{value.length} chars</span> <span className="hidden sm:block"> <kbd className="rounded bg-fd-card px-1.5 py-0.5 font-mono">→</kbd> to accept ghost </span> </div> </div> </div> </SmartProvider> );}Stop sequences
Multiline completions default to stopping at ['\n\n'] — one paragraph. Override per-component:
<SmartTextarea
value={v}
onChange={setV}
stop={['\n\n', 'Sincerely', '\n---']}
/>Auto-resize
<SmartTextarea value={v} onChange={setV} autoResize rows={2} />Grows on input + on accept. Pair with a max-height in CSS if you want a scroll-after-N-lines behavior.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| value* | string | — | Controlled textarea value. |
| onChange* | (value: string) => void | — | Called on user input or accept. |
| context | string | — | Optional context string included in the prompt. |
| minChars | number | 3 | Minimum chars before suggestions fetch. |
| debounceMs | number | 300 | Debounce window in ms. |
| stream | boolean | false | Use the streaming capability for faster first paint. |
| disableAI | boolean | false | Disable all AI calls. |
| acceptKey | string | 'ArrowRight' | Accept key. Do NOT use "Enter" — it would hijack newline. |
| dismissKey | string | 'Escape' | Key that dismisses the ghost. |
| maxTokens | number | 64 | Max tokens for the completion call. |
| stop | string[] | ['\n\n'] | Stop sequences for the completion. |
| autoResize | boolean | false | Auto-grow the textarea to fit content. |
| renderGhost | (suggestion: string) => ReactNode | — | Render-prop override for the ghost text. |
| ghostClassName | string | — | Class name applied to the ghost text span. |
| ghostStyle | CSSProperties | — | Inline style applied to the ghost text span. |
| wrapperClassName | string | — | Class name applied to the outer wrapper. |
| onAccept | (accepted, finalValue) => void | — | Called when the suggestion is accepted. |
| onGhostChange | (suggestion: string) => void | — | Called whenever the visible ghost changes. |
| ...rest | TextareaHTMLAttributes<HTMLTextAreaElement> | — | All native textarea props minus value, onChange, defaultValue. |
Mobile notes
- Do not set
acceptKey="Enter"— it hijacks newline. - For mobile, use the imperative
accept()via theSmartTextareaHandleref (same shape asSmartTextboxHandle).