{"version":3,"file":"suggestion-menu.cjs","sources":["../../../components/editor/suggestion-menu.tsx"],"sourcesContent":["'use client';\n\nimport { cx } from 'class-variance-authority';\nimport type { CSSProperties, ReactNode } from 'react';\nimport { Cell } from '../menu/cell';\nimport { Popover } from '../popover';\nimport { Skeleton } from '../skeleton';\nimport styles from './editor.module.css';\n\nexport interface SuggestionMenuItem {\n  id: string;\n  label: string;\n  type?: string;\n  icon?: ReactNode;\n  trailing?: ReactNode;\n  group?: string;\n  disabled?: boolean;\n  data?: unknown;\n}\n\n/** Rendered in first-appearance order; the leading group has no label. */\nexport interface SuggestionGroup {\n  label?: string;\n  items: SuggestionMenuItem[];\n}\n\n/** A zero-width caret rect, re-measured by the positioner as the caret moves. */\nexport interface SuggestionAnchor {\n  getBoundingClientRect: () => DOMRect;\n  /**\n   * The editing host. A virtual element has nothing for the positioner to\n   * observe on its own, so without this the menu would not follow the trigger\n   * when the composer grows a line or its scroll ancestors move.\n   */\n  contextElement?: Element;\n}\n\nexport interface SuggestionMenuProps {\n  open: boolean;\n  anchor: SuggestionAnchor | null;\n  /** Listbox id, referenced by the editor's `aria-controls`. */\n  id: string;\n  groups: SuggestionGroup[];\n  /** Index into the flattened item list, or -1. */\n  highlightedIndex: number;\n  onHighlightChange: (index: number) => void;\n  onSelect: (item: SuggestionMenuItem, index: number) => void;\n  onOpenChange: (open: boolean) => void;\n  loading?: boolean;\n  /** @defaultValue 3 */\n  loadingRowCount?: number;\n  /** @defaultValue \"No results\" */\n  emptyMessage?: ReactNode;\n  /** Width the popup takes, in pixels — the composer frame's width. */\n  width?: number;\n  'aria-label'?: string;\n}\n\n/** Row ids are derived, so the editor can name the highlighted one. */\nexport function suggestionOptionId(listId: string, index: number): string {\n  return `${listId}-option-${index}`;\n}\n\nexport function SuggestionMenu({\n  open,\n  anchor,\n  id,\n  groups,\n  highlightedIndex,\n  onHighlightChange,\n  onSelect,\n  onOpenChange,\n  loading = false,\n  loadingRowCount = 3,\n  emptyMessage = 'No results',\n  width,\n  'aria-label': ariaLabel = 'Suggestions'\n}: SuggestionMenuProps) {\n  const total = groups.reduce((count, group) => count + group.items.length, 0);\n  let cursor = -1;\n\n  return (\n    <Popover open={open && anchor !== null} onOpenChange={onOpenChange}>\n      <Popover.Content\n        anchor={anchor ?? undefined}\n        align='start'\n        side='bottom'\n        sideOffset={6}\n        // Focus never leaves the editor — the query lives in the document\n        // because it is the text the chip replaces.\n        initialFocus={false}\n        finalFocus={false}\n        className={styles.suggestionMenu}\n        style={\n          {\n            '--suggestion-menu-width': width ? `${width}px` : undefined\n          } as CSSProperties\n        }\n      >\n        <div id={id} role='listbox' aria-label={ariaLabel}>\n          {loading && total === 0\n            ? Array.from({ length: loadingRowCount }).map((_, index) => (\n                <div\n                  key={index}\n                  className={styles.suggestionLoadingRow}\n                  aria-hidden='true'\n                >\n                  <Skeleton height='var(--rs-space-4)' />\n                </div>\n              ))\n            : null}\n\n          {!loading && total === 0 ? (\n            <div className={styles.suggestionEmpty}>{emptyMessage}</div>\n          ) : null}\n\n          {groups.map(group => (\n            <div\n              key={group.label ?? '__ungrouped__'}\n              className={styles.suggestionGroup}\n              role='presentation'\n            >\n              {group.label ? (\n                <div\n                  className={styles.suggestionGroupLabel}\n                  role='presentation'\n                >\n                  {group.label}\n                </div>\n              ) : null}\n              {group.items.map(item => {\n                cursor += 1;\n                const index = cursor;\n                const highlighted = index === highlightedIndex;\n                return (\n                  <Cell\n                    key={`${item.type ?? ''}:${item.id}`}\n                    id={suggestionOptionId(id, index)}\n                    role='option'\n                    aria-selected={highlighted}\n                    aria-disabled={item.disabled || undefined}\n                    data-highlighted={highlighted ? '' : undefined}\n                    className={cx(styles.suggestionRow)}\n                    leadingIcon={item.icon}\n                    trailingIcon={item.trailing}\n                    onPointerMove={() => {\n                      if (!item.disabled) onHighlightChange(index);\n                    }}\n                    // Keep the caret: a press inside the menu must not move\n                    // focus out of the editor.\n                    onPointerDown={event => event.preventDefault()}\n                    onClick={() => {\n                      if (!item.disabled) onSelect(item, index);\n                    }}\n                  >\n                    {item.label}\n                  </Cell>\n                );\n              })}\n            </div>\n          ))}\n        </div>\n      </Popover.Content>\n    </Popover>\n  );\n}\n\nSuggestionMenu.displayName = 'SuggestionMenu';\n"],"names":[],"mappings":";;;;;;;;;;AA0DA;AACgB;AACd;AACF;;;AAkBE;AAEA;;;AASM;;AAMoB;AAKhB;AASA;;;AAuBI;;;;;;;AAiBI;;AAEsB;AACtB;AAKN;AAOd;AAEA;;;"}