{"version":3,"file":"prompt-input-editor.cjs","sources":["../../../components/prompt-input/prompt-input-editor.tsx"],"sourcesContent":["'use client';\n\nimport { useMergedRefs } from '@base-ui/utils/useMergedRefs';\nimport { cx } from 'class-variance-authority';\nimport {\n  type ComponentProps,\n  Fragment,\n  useCallback,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport {\n  deriveDocDetails,\n  docFromMarkup,\n  editorStyles,\n  SuggestionMenu,\n  type SuggestionState,\n  useEditor\n} from '../editor';\nimport styles from './prompt-input.module.css';\nimport {\n  type PromptInputInputApi,\n  usePromptInputContext\n} from './prompt-input-context';\nimport { useMentionMenu, useMentionResolution } from './use-mention-menu';\n\nexport interface PromptInputEditorProps\n  extends Omit<\n    ComponentProps<'div'>,\n    'contentEditable' | 'children' | 'dangerouslySetInnerHTML' | 'role'\n  > {\n  /**\n   * Shown while the composer is empty.\n   * @defaultValue \"Write a message…\"\n   */\n  placeholder?: string;\n  /** Disables just the editor. Inherits the root `disabled` by default. */\n  disabled?: boolean;\n  /**\n   * Cap on the derived plain text — a chip counts as its label. Enforced by a\n   * transaction filter, so paste and IME are covered, not just keystrokes.\n   */\n  maxLength?: number;\n  /**\n   * Valid on a contentEditable, unlike `maxLength`.\n   * @defaultValue true\n   */\n  spellCheck?: boolean;\n}\n\n/**\n * The ProseMirror sibling to `PromptInput.Textarea`: same outward contract —\n * Enter submits, Shift+Enter breaks, placeholder, auto-grow, `disabled`, frame\n * focus — on a contentEditable that can host inline mention chips.\n */\nexport function PromptInputEditor({\n  className,\n  placeholder = 'Write a message…',\n  disabled,\n  maxLength,\n  spellCheck = true,\n  ref,\n  ...props\n}: PromptInputEditorProps) {\n  const context = usePromptInputContext('Editor');\n  const listboxId = useId();\n  const resolvedDisabled = disabled ?? context.disabled;\n\n  const [suggestion, setSuggestion] = useState<SuggestionState | null>(null);\n  const [frameWidth, setFrameWidth] = useState<number | undefined>(undefined);\n\n  const setValueRef = useRef(context.setValue);\n  setValueRef.current = context.setValue;\n  const requestSubmitRef = useRef(context.requestSubmit);\n  requestSubmitRef.current = context.requestSubmit;\n  const registry = context.mentions;\n\n  // Broken out of the option object so the menu, which needs `actions`, can\n  // still supply the key handler the editor plugin calls.\n  const keyDownRef = useRef<\n    ((event: KeyboardEvent, state: SuggestionState) => boolean) | null\n  >(null);\n\n  const { hostRef, initialHtml, viewRef, mentionPortals, actions } = useEditor({\n    initialMarkup: context.value,\n    placeholder,\n    disabled: resolvedDisabled,\n    spellCheck,\n    maxLength,\n    getTriggers: () => registry.triggers(),\n    onChange: details =>\n      setValueRef.current(details.markup, {\n        text: details.text,\n        mentions: details.mentions\n      }),\n    onSubmit: () => requestSubmitRef.current(),\n    onSuggestionChange: setSuggestion,\n    onSuggestionKeyDown: (event, state) =>\n      keyDownRef.current?.(event, state) ?? false\n  });\n\n  const menu = useMentionMenu({\n    viewRef,\n    actions,\n    registry,\n    suggestion,\n    disabled: resolvedDisabled,\n    listboxId\n  });\n  keyDownRef.current = menu.handleKeyDown;\n\n  useMentionResolution(registry, context.details.mentions, actions);\n\n  const api = useMemo<PromptInputInputApi>(\n    () => ({\n      focus: () => actions.focus(),\n      setMarkup: markup => actions.setMarkup(markup),\n      deriveExternal: markup => {\n        const derived = deriveDocDetails(docFromMarkup(markup));\n        return { text: derived.text, mentions: derived.mentions };\n      },\n      getMessage: () => {\n        const details = actions.getDetails();\n        return {\n          markup: details.markup,\n          text: details.text,\n          mentions: details.mentions\n        };\n      },\n      insertMention: (item, options) => {\n        const trigger = options?.trigger ?? registry.triggers()[0] ?? '@';\n        const type = item.type ?? 'mention';\n        registry.remember(trigger, { ...item, type });\n        actions.insertMention({\n          id: item.id,\n          label: item.label,\n          type,\n          trigger\n        });\n      }\n    }),\n    [actions, registry]\n  );\n\n  const registerInput = context.registerInput;\n  const register = useCallback(\n    (node: HTMLDivElement | null) => {\n      hostRef(node);\n      registerInput(node, api, 'editor');\n    },\n    [api, hostRef, registerInput]\n  );\n\n  const mergedRef = useMergedRefs(register, ref);\n\n  // A caret is a zero-width anchor, so the menu cannot size itself from\n  // `--anchor-width`; it takes the composer's width instead, re-measured as the\n  // panel resizes. Deliberately a passive effect rather than a layout one: the\n  // frame is this part's ancestor, and React attaches an ancestor's ref *after*\n  // running a descendant's layout effects — measuring there would read a null\n  // frame once and never look again.\n  const frameRef = context.frameRef;\n  useEffect(() => {\n    const frame = frameRef.current;\n    if (!frame || typeof ResizeObserver === 'undefined') return;\n    const measure = () => {\n      const width = frame.getBoundingClientRect().width;\n      setFrameWidth(width > 0 ? width : undefined);\n    };\n    measure();\n    const observer = new ResizeObserver(measure);\n    observer.observe(frame);\n    return () => observer.disconnect();\n  }, [frameRef]);\n\n  const hasMentions = registry.triggers().length > 0;\n\n  return (\n    <>\n      <div\n        {...props}\n        ref={mergedRef}\n        className={cx(styles.editor, editorStyles.editor, className)}\n        role='textbox'\n        aria-multiline='true'\n        aria-disabled={resolvedDisabled || undefined}\n        aria-autocomplete={hasMentions ? 'list' : undefined}\n        aria-expanded={hasMentions ? menu.open : undefined}\n        aria-controls={hasMentions && menu.open ? listboxId : undefined}\n        aria-activedescendant={menu.activeOptionId}\n        data-disabled={resolvedDisabled || undefined}\n        data-empty={context.empty || undefined}\n        spellCheck={spellCheck}\n        // ProseMirror takes this subtree over on mount. Until then it holds the\n        // derived plain text of the value, so a restored draft is readable on\n        // the first paint instead of popping in.\n        suppressHydrationWarning\n        dangerouslySetInnerHTML={initialHtml}\n      />\n\n      {mentionPortals.map(portal => {\n        const item = registry.lookup(\n          portal.attrs.trigger,\n          portal.attrs.type,\n          portal.attrs.id\n        );\n        return (\n          <Fragment key={portal.id}>\n            {item?.icon ? createPortal(item.icon, portal.iconTarget) : null}\n            {item?.trailing\n              ? createPortal(item.trailing, portal.trailingTarget)\n              : null}\n          </Fragment>\n        );\n      })}\n\n      {hasMentions ? (\n        <SuggestionMenu\n          open={menu.open}\n          anchor={menu.anchor}\n          id={listboxId}\n          groups={menu.groups}\n          highlightedIndex={menu.highlightedIndex}\n          onHighlightChange={menu.setHighlightedIndex}\n          onSelect={menu.select}\n          onOpenChange={next => {\n            if (!next) menu.close();\n          }}\n          loading={menu.loading}\n          loadingRowCount={menu.loadingRowCount}\n          emptyMessage={menu.emptyMessage}\n          width={frameWidth}\n        />\n      ) : null}\n    </>\n  );\n}\n\nPromptInputEditor.displayName = 'PromptInput.Editor';\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;AAsDA;;;;AAIG;AACG;AASJ;AACA;AACA;;;;AAMA;;AAEA;AACA;;;AAIA;AAIA;;;AAGE;;;AAGA;AACA;;;;AAKA;AACA;AACA;AAED;;;;;;AAOC;;AAED;AACD;;AAIA;AAEI;;;;AAIE;;;AAGA;;;;;;;AAOF;AACE;AACA;AACA;;;;;;AAMC;;AAEJ;AAIH;AACA;;AAGI;;;;;;;;;AAaJ;;AAEE;AACA;;;;AAGE;AACF;AACA;AACA;AACA;AACA;AACF;;AAIA;;;;AAmBM;;;;;;AA8BI;;AACF;AASV;AAEA;;"}