TextEditorCard

Review, edit, copy, or download one plain-text or Markdown draft.

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.

Proposal notes

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-card

The 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.md

It 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.

lib/fable-ui/tools/show-text-editor-tool.ts
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

StateBehavior
ReadyShows the editable textarea when editable is true; Copy and Download use the current local draft.
LoadingShows a preparing status and skeleton; actions stay unavailable without meaningful content.
StreamingShows generated content read-only until the part completes; meaningful partial text can still be copied or downloaded.
EmptyExplains that no text is available.
ErrorShows the error and retains any partial text as selectable, copyable, downloadable content.
DisabledKeeps 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.

lib/fable-ui/manifests/show-text-editor.md
---
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.