TextEditorCard presents one self-contained plain-text or Markdown draft. It is useful when an AI response should become a small user-owned document without turning into rich-text editing or a file write.
52 / 1200 characters
import { TextEditorCard } from "@/components/fable-ui/text-editor-card"
export function ProposalDraft() {
return (
<TextEditorCard
label="Proposal notes"
content="Start with a short summary."
format="markdown"
filename="proposal-notes.md"
maxLength={1200}Installation#
pnpm dlx shadcn@latest add shobky/fable-ui/text-editor-cardThe registry item installs the component, its small uncontrolled-draft hook, copy helper, AI SDK tool, and routing manifest:
components/fable-ui/text-editor-card/index.ts
components/fable-ui/text-editor-card/text-editor-card.tsx
components/fable-ui/text-editor-card/use-plain-text-draft.ts
hooks/use-copy-to-clipboard.ts
lib/fable-ui/tools/show-text-editor-tool.ts
lib/fable-ui/manifests/show-text-editor.mdIt uses shadcn alert, button, card, empty, field, skeleton, textarea, and tooltip primitives, plus the shared Fable core registry dependency.
Usage#
import { TextEditorCard } from "@/components/fable-ui/text-editor-card"
export function ProposalDraft() {
return (
<TextEditorCard
label="Proposal notes"
content={"## Summary\n\nStart with the clearest recommendation."}
format="markdown"
filename="proposal-notes.md"
maxLength={1200}
onContentChange={(content) => {
// Persist only after the user leaves the textarea.
console.log(content)
}}
/>
)
}The textarea is intentionally uncontrolled while ready. onContentChange fires on blur, not on every keystroke. Set editable={false} or isDisabled to keep a selectable read-only draft. maxLength is a visible soft guide: it neither truncates text nor disables copy or download.
The compact Copy and Download icons sit at the card’s logical inline end. They have accessible names and tooltips; the default “Text editor” title stays semantic but is visually hidden because the content itself supplies the hierarchy.
Tool definition#
The AI SDK tool name is show_text_editor. Its input accepts:
{
label?: string
content: string
format?: "plain" | "markdown"
filename?: string
editable?: boolean
direction?: "ltr" | "rtl" | "auto"
maxLength?: number
}Use it for one display-ready note, document, or Markdown draft. Do not use it for rich text, collaborative editing, source code, email recipients, forms, files, or host-side mutations. The host owns persistence and authorization.
import { tool } from "ai"
import { z } from "zod"
import {
TextEditorCard,
type TextEditorCardProps,
} from "@/components/fable-ui/text-editor-card"
import { defineFableComponent } from "@/lib/fable-ui/core/definitions"
const textEditorFormatSchema = z.enum(["plain", "markdown"])
const textDirectionSchema = z.enum(["ltr", "rtl", "auto"])
export const showTextEditorInputSchema = z.object({
label: z.string().min(1).optional(),
content: z.string(),
format: textEditorFormatSchema.default("plain"),
filename: z.string().min(1).optional(),
editable: z.boolean().default(true),
direction: textDirectionSchema.default("auto"),
maxLength: z.number().int().positive().optional(),
})
export type ShowTextEditorInput = z.infer<typeof showTextEditorInputSchema>
function getPartialTextEditorProps(input: unknown): TextEditorCardProps {
const partial =
input && typeof input === "object" ? (input as Record<string, unknown>) : {}
return {
label: typeof partial.label === "string" ? partial.label : "Text editor",
content: typeof partial.content === "string" ? partial.content : "",
format: partial.format === "markdown" ? "markdown" : "plain",
filename:
typeof partial.filename === "string" ? partial.filename : undefined,
editable: typeof partial.editable === "boolean" ? partial.editable : true,
direction:
partial.direction === "ltr" ||
partial.direction === "rtl" ||
partial.direction === "auto"
? partial.direction
: "auto",
maxLength:
typeof partial.maxLength === "number" &&
Number.isInteger(partial.maxLength) &&
partial.maxLength > 0
? partial.maxLength
: undefined,
isStreaming: true,
}
}
export function createShowTextEditorTool() {
return tool({
description:
"Show one plain-text or Markdown draft for review, local editing, copying, or download. Use for a self-contained document or note, not rich text, multi-recipient email, code, forms, or host-side file editing.",
inputSchema: showTextEditorInputSchema,
execute: async (input) => input,
})
}
export const showTextEditor = defineFableComponent({
name: "show_text_editor",
schema: showTextEditorInputSchema,
tool: createShowTextEditorTool(),
renderer: {
Component: TextEditorCard,
loadingProps: { label: "Text editor", content: "", isLoading: true },
streamingProps: getPartialTextEditorProps,
emptyProps: { label: "Text editor", content: "" },
errorProps: (description, part) => ({
...getPartialTextEditorProps(part?.input),
isStreaming: false,
error: { title: "Text editor unavailable", description },
}),
toProps: (data: ShowTextEditorInput) => data,
},
})
States#
| State | Behavior |
|---|---|
| Ready | Shows the editable textarea when editable is true; Copy and Download use the current local draft. |
| Loading | Shows a preparing status and skeleton; actions stay unavailable without meaningful content. |
| Streaming | Shows generated content read-only until the part completes; meaningful partial text can still be copied or downloaded. |
| Empty | Explains that no text is available. |
| Error | Shows the error and retains any partial text as selectable, copyable, downloadable content. |
| Disabled | Keeps ready text read-only while leaving meaningful content available for copy and download. |
RTL and accessibility#
Use direction="rtl" or direction="ltr" when known. With auto, the card detects Arabic and Hebrew characters for read-only presentation while preserving the host document direction elsewhere. Actions use CardAction at the logical inline end, so they follow RTL layout without hard-coded left/right positioning.
Each textarea has a visible label, the icon-only actions have screen-reader labels and tooltips, loading status is announced, and partial/error content remains available rather than disappearing.
Manifest and caveats#
The manifest explains when the model should choose a focused editable draft rather than a form, email, or code block.
---
tool: show_text_editor
type: registry:component
---
# show_text_editor
Use `show_text_editor` for one self-contained plain-text or Markdown draft that the user can review, edit locally after generation completes, copy, or download.
Include the complete `content`. Use `format: "markdown"` only when the text is Markdown. Set `editable: false` for a read-only historical or host-controlled draft. Use `direction` when the content direction is known; otherwise leave it as `auto`.
Avoid it for rich text, multiple email recipients, source code, structured forms, live collaborative editing, file writes, or data retrieval. The card does not persist edits or mutate host files.
`maxLength` is a visible soft guidance limit, not a truncation or validation rule. Partial and interrupted text stays available to copy or download when meaningful.
This card only manages local browser state. It does not save, sync, sanitize rich text, or write a file on its own.