{"version":3,"file":"ChatComposer.cjs","names":[],"sources":["../../../src/components/Chat/ChatComposer.tsx"],"sourcesContent":["import {\n    forwardRef,\n    useImperativeHandle,\n    useLayoutEffect,\n    useRef,\n    useState,\n    type FormEvent,\n    type KeyboardEvent,\n    type ReactNode,\n    type TextareaHTMLAttributes,\n} from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { chatStrings } from \"./chat-groups\";\nimport styles from \"./Chat.module.css\";\n\n/** DOM attributes the composer redefines. */\ntype OverriddenDomProps = \"onSubmit\" | \"value\" | \"defaultValue\" | \"rows\";\n\nexport interface ChatComposerProps extends Omit<\n    TextareaHTMLAttributes<HTMLTextAreaElement>,\n    OverriddenDomProps\n> {\n    /** Called with the trimmed text. The field clears only when this does not throw. */\n    onSend: (text: string) => void | Promise<void>;\n    /** Locale for the placeholder and the send label. Default `\"pt-BR\"`. */\n    locale?: \"pt-BR\" | \"en\";\n    /** Rendered to the left of the send button — an attach button, an emoji picker. */\n    actions?: ReactNode;\n    /** Largest height the field grows to, in lines. Default 6. */\n    maxRows?: number;\n    /** Send label. Defaults to the locale string. */\n    sendLabel?: string;\n    /**\n     * Called when `onSend` rejects. The draft is kept either way.\n     *\n     * Without it the rejection is swallowed after the draft is preserved, because\n     * the alternative is worse: re-throwing out of a DOM event handler surfaces as\n     * an unhandled promise rejection — console noise for the developer, a hit in\n     * whatever crash reporter the app runs, and nothing the user can act on. The\n     * visible signal is the text still sitting in the field; wire this to a toast\n     * to say why.\n     */\n    onError?: (error: unknown) => void;\n}\n\n/** Imperative handle, so a thread can focus the field after retrying a message. */\nexport interface ChatComposerHandle {\n    focus: () => void;\n    /** Replace the draft — used to put a failed message back in the field. */\n    setValue: (text: string) => void;\n}\n\n/**\n * The message field of a thread: a textarea that grows with its content, sends on\n * `Enter` and keeps `Shift+Enter` for a newline.\n *\n * Uncontrolled on purpose. A chat draft changes on every keystroke, and lifting\n * that into app state re-renders the whole thread per character — the one place\n * where \"controlled by default\" costs something visible. Apps that need the draft\n * (a persisted composer, a slash-command menu) read it from `onChange` or drive it\n * through the ref.\n *\n * @example\n * <ChatComposer onSend={(text) => api.post(\"/messages\", { body: { text } })} />\n */\nexport const ChatComposer = forwardRef<ChatComposerHandle, ChatComposerProps>(function ChatComposer(\n    {\n        onSend,\n        locale = \"pt-BR\",\n        actions,\n        maxRows = 6,\n        sendLabel,\n        onError,\n        className,\n        disabled,\n        placeholder,\n        onKeyDown,\n        onChange,\n        ...rest\n    },\n    ref,\n) {\n    const strings = chatStrings(locale);\n    const textarea = useRef<HTMLTextAreaElement | null>(null);\n    const [value, setValue] = useState(\"\");\n    const [busy, setBusy] = useState(false);\n\n    useImperativeHandle(ref, () => ({\n        focus: () => textarea.current?.focus(),\n        setValue: (text: string) => {\n            setValue(text);\n            textarea.current?.focus();\n        },\n    }));\n\n    /**\n     * Grow the field to fit its content, up to `maxRows`.\n     *\n     * Measured from `scrollHeight` after resetting the height, because\n     * `scrollHeight` on an element that is already tall enough reports the\n     * current height and the field would never shrink back.\n     */\n    useLayoutEffect(() => {\n        const node = textarea.current;\n        if (!node) return;\n        node.style.height = \"auto\";\n        const lineHeight = Number.parseFloat(getComputedStyle(node).lineHeight) || 20;\n        const max = lineHeight * maxRows;\n        node.style.height = `${Math.min(node.scrollHeight, max)}px`;\n        node.style.overflowY = node.scrollHeight > max ? \"auto\" : \"hidden\";\n    }, [value, maxRows]);\n\n    const submit = async (): Promise<void> => {\n        const text = value.trim();\n        if (!text || busy || disabled) return;\n        setBusy(true);\n        try {\n            await onSend(text);\n            setValue(\"\");\n        } catch (error) {\n            onError?.(error);\n        } finally {\n            setBusy(false);\n        }\n    };\n\n    const handleSubmit = (event: FormEvent): void => {\n        event.preventDefault();\n        void submit();\n    };\n\n    /**\n     * `Enter` sends, `Shift+Enter` breaks the line.\n     *\n     * The IME check is not optional: while composing Japanese or Korean, `Enter`\n     * confirms the candidate word and `keyCode === 229` marks that keystroke.\n     * Sending there would post half a word and eat the confirmation.\n     */\n    const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>): void => {\n        onKeyDown?.(event);\n        if (event.defaultPrevented) return;\n        if (event.key !== \"Enter\" || event.shiftKey) return;\n        if (event.nativeEvent.isComposing || event.keyCode === 229) return;\n        event.preventDefault();\n        void submit();\n    };\n\n    return (\n        <form className={cn(styles.composer, className)} onSubmit={handleSubmit}>\n            <textarea\n                {...rest}\n                ref={textarea}\n                className={styles.field}\n                rows={1}\n                value={value}\n                disabled={disabled}\n                placeholder={placeholder ?? strings.placeholder}\n                onChange={(event) => {\n                    setValue(event.target.value);\n                    onChange?.(event);\n                }}\n                onKeyDown={handleKeyDown}\n            />\n            <div className={styles.composerActions}>\n                {actions}\n                <button\n                    type=\"submit\"\n                    className={styles.send}\n                    disabled={disabled || busy || value.trim() === \"\"}\n                >\n                    {sendLabel ?? strings.send}\n                </button>\n            </div>\n        </form>\n    );\n});\n"],"mappings":"0JAmEA,IAAa,GAAA,EAAe,EAAA,WAAA,CAAkD,SAC1E,CACI,SACA,SAAS,QACT,UACA,UAAU,EACV,YACA,UACA,YACA,WACA,cACA,YACA,WACA,GAAG,GAEP,EACF,CACE,IAAM,EAAU,EAAA,YAAY,CAAM,EAC5B,GAAA,EAAW,EAAA,OAAA,CAAmC,IAAI,EAClD,CAAC,EAAO,IAAA,EAAY,EAAA,SAAA,CAAS,EAAE,EAC/B,CAAC,EAAM,IAAA,EAAW,EAAA,SAAA,CAAS,EAAK,GAEtC,EAAA,EAAA,oBAAA,CAAoB,OAAY,CAC5B,UAAa,EAAS,SAAS,MAAM,EACrC,SAAW,GAAiB,CACxB,EAAS,CAAI,EACb,EAAS,SAAS,MAAM,CAC5B,CACJ,EAAE,GASF,EAAA,EAAA,gBAAA,KAAsB,CAClB,IAAM,EAAO,EAAS,QACtB,GAAI,CAAC,EAAM,OACX,EAAK,MAAM,OAAS,OAEpB,IAAM,GADa,OAAO,WAAW,iBAAiB,CAAI,CAAC,CAAC,UAAU,GAAK,IAClD,EACzB,EAAK,MAAM,OAAS,GAAG,KAAK,IAAI,EAAK,aAAc,CAAG,EAAE,IACxD,EAAK,MAAM,UAAY,EAAK,aAAe,EAAM,OAAS,QAC9D,EAAG,CAAC,EAAO,CAAO,CAAC,EAEnB,IAAM,EAAS,SAA2B,CACtC,IAAM,EAAO,EAAM,KAAK,EACpB,MAAC,GAAQ,GAAQ,GACrB,GAAQ,EAAI,EACZ,GAAI,CACA,MAAM,EAAO,CAAI,EACjB,EAAS,EAAE,CACf,OAAS,EAAO,CACZ,IAAU,CAAK,CACnB,QAAU,CACN,EAAQ,EAAK,CACjB,CARY,CAShB,EAEM,EAAgB,GAA2B,CAC7C,EAAM,eAAe,EACrB,EAAY,CAChB,EASM,EAAiB,GAAoD,CACvE,IAAY,CAAK,EACb,GAAM,mBACN,EAAM,MAAQ,SAAW,EAAM,UAC/B,EAAM,YAAY,aAAe,EAAM,UAAY,MACvD,EAAM,eAAe,EACrB,EAAY,GAChB,EAEA,OACI,EAAA,EAAA,KAAA,CAAC,OAAD,CAAM,UAAW,EAAA,GAAG,EAAA,QAAO,SAAU,CAAS,EAAG,SAAU,EAA3D,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,WAAD,CACI,GAAI,EACJ,IAAK,EACL,UAAW,EAAA,QAAO,MAClB,KAAM,EACC,QACG,WACV,YAAa,GAAe,EAAQ,YACpC,SAAW,GAAU,CACjB,EAAS,EAAM,OAAO,KAAK,EAC3B,IAAW,CAAK,CACpB,EACA,UAAW,CACd,CAAA,GACD,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,gBAAvB,SAAA,CACK,GACD,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,KAClB,SAAU,GAAY,GAAQ,EAAM,KAAK,IAAM,GAE9C,SAAA,GAAa,EAAQ,IAClB,CAAA,CACP,CACH,CAAA,CAAA,GAEd,CAAC"}