import { Paperclip } from 'lucide-react' import { useEffect, useMemo, useRef, useState } from 'react' import styled from 'styled-components' import Block from '../Primitives/Block/Block' import PrimitiveTextarea from '../Primitives/PrimitiveTextArea' import PrimitiveButton from '../Primitives/PrimitiveButton/PrimitiveButton' import { FOUNDATION_THEME } from '../../tokens' import { ChatInputTokensType } from './chatInput.tokens' import type { AttachedFile } from './types' import type { MenuProps } from '../Menu/types' import AttachmentFile from './AttachmentFile' import { truncatePlaceholder } from './utils' import { useResponsiveTokens } from '../../hooks/useResponsiveTokens' const HiddenScrollbarTextarea = styled(PrimitiveTextarea)` /* Hide scrollbar for Firefox */ scrollbar-width: none; /* Hide scrollbar for Chrome, Safari, and Edge */ &::-webkit-scrollbar { display: none; } /* Hide scrollbar for IE and Edge */ -ms-overflow-style: none; ` type MobileChatInputProps = { value: string onChange?: (value: string) => void slot1?: React.ReactNode slot2?: React.ReactNode placeholder?: string attachedFiles?: AttachedFile[] handleAttachClick?: () => void onFileRemove?: (fileId: string) => void onFileClick?: (file: AttachedFile) => void overflowMenuProps?: Partial } const MobileChatInput: React.FC = ({ value, onChange, slot1, slot2, placeholder, attachedFiles, handleAttachClick, onFileRemove, onFileClick, overflowMenuProps, }) => { const containerRef = useRef(null!) const tokens = useResponsiveTokens('CHAT_INPUT') const textareaRef = useRef(null) const [isFocused, setIsFocused] = useState(false) const [truncatedPlaceholder, setTruncatedPlaceholder] = useState< string | undefined >(placeholder) const handleTextareaChange = ( e: React.ChangeEvent ) => { onChange?.(e.target.value) } // Update truncated placeholder when textarea resizes useEffect(() => { const el = textareaRef.current if (!el) return const updatePlaceholder = () => { setTruncatedPlaceholder(truncatePlaceholder(el, placeholder)) } updatePlaceholder() const resizeObserver = new ResizeObserver(updatePlaceholder) resizeObserver.observe(el) return () => resizeObserver.disconnect() }, [placeholder]) useEffect(() => { const el = textareaRef.current if (!el) return el.style.height = 'auto' const maxHeight = parseFloat(String(tokens.maxHeight).replace('px', '')) el.style.height = `${Math.min(el.scrollHeight, maxHeight)}px` const defaultHeight = parseFloat( String(tokens.minHeight).replace('px', '') ) if (el.scrollHeight > defaultHeight) { el.style.borderRadius = tokens.borderRadius.focus as string } else { el.style.borderRadius = tokens.borderRadius.default as string } }, [value, tokens]) const attachmentButtonDimensions = useMemo(() => { // Button is fixed at 44px (FOUNDATION_THEME.unit[44]) as defined in PrimitiveButton const buttonWidth = FOUNDATION_THEME.unit[44] // Gap between button and textarea (using unit[8] from tokens) const gap = FOUNDATION_THEME.unit[8] const total = `calc(${buttonWidth} + ${gap})` return { buttonWidth, gap, total, } }, []) return ( {attachedFiles && attachedFiles.length > 0 && ( )} {slot2} { setIsFocused(true) e.currentTarget.style.borderRadius = tokens .borderRadius.focus as string e.currentTarget.style.border = tokens.border .focus as string }} onBlur={(e) => { setIsFocused(false) e.currentTarget.style.border = tokens.border .default as string }} /> {slot1} ) } export default MobileChatInput