'use client'; import { useCallback, useMemo, useRef } from 'react'; import { LazyMarkdownEditor as MarkdownEditor, type MarkdownEditorHandle, type MentionConfig, type SlashCommandInfo, } from '@djangocfg/ui-tools/markdown-editor'; import { useRegisterComposer } from '../hooks/useAutoFocusOnStreamEnd'; import type { ComposerSize, ComposerTextareaProps } from './types'; /** * Single-row min-height FLOOR per composer size (px). * * This is a floor, NOT the row height — the empty `

`'s own line box * defines the real single-row height. The floor must stay BELOW one line * box (15px text × line-height) so it can never inflate the editor past * one line: when a host overrides the editor leading (e.g. cmdop pins * `.ProseMirror` to `leading-normal` ≈ 22.5px), a 24/28px min-height used * to sit ABOVE the line box and the empty composer read as having a * phantom extra line under the caret. Keeping every floor ≤ the tightest * expected single line (≈22px) means the line box always wins and the * single-row composer is exactly one line tall in every host. */ const MIN_HEIGHT: Record = { sm: 18, md: 20, lg: 22, }; export interface ComposerRichTextareaProps extends ComposerTextareaProps { /** `@`-mention autocomplete config. Omit for a plain rich textarea. */ mentions?: MentionConfig; /** * Slash-command verb list. When set, a leading `/verb` in the editor * is painted as an atom chip (same primary-tinted styling as the * plain `` mirror). The chat composer's slash * menu already lives outside the editor and keeps working without * this prop — pass the verb list here when you also want the * in-editor highlight to match. * * Structurally compatible with `SlashCommand` from * `@djangocfg/ui-tools/chat` — passing the same array you wire into * `composerSlots.slashCommands` is the normal pattern. * * Note: like `mentions`, the underlying TipTap extension is captured * once on first render. If you may register slash commands later, * pass `[]` from the very first render and mutate the list in place. */ slashCommands?: readonly SlashCommandInfo[]; } /** * Drop-in TipTap-backed textarea for the composer's Tier B `Textarea` * slot. Pre-wired with chat defaults — `unstyled` (the composer surface * draws the frame), no toolbar, size-matched single-row height, and * Enter-to-send. The host passes only `mentions` if it needs `@` * autocomplete; everything else flows from `ComposerTextareaProps`. * * @example * }} * /> */ export function ComposerRichTextarea({ composer, placeholder, disabled, size, className, mentions, slashCommands, }: ComposerRichTextareaProps) { // Tiptap captures the mentions object once on first render — keep its // identity stable so the suggestion plugin stays wired. const stableMentions = useMemo(() => mentions, [mentions]); // `slashCommands` flows through every render — the editor's // SlashCommandNode is registered once on first mount (gated on // `slashCommands !== undefined`) and the current list is read via a // ref by the value-sync effect, so runtime additions / removals // still light up the chip. // The TipTap editor has no `