ReferenceMentionList.tsx3.4 KBView on GitHub
'use client';

import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
import { cn } from '@/lib/utils';
import { referenceMeta, referenceLabel, type ReferenceOption } from './references';

export interface ReferenceMentionListRef {
  onKeyDown: (data: { event: KeyboardEvent }) => boolean;
}

interface ReferenceMentionListProps {
  items: ReferenceOption[];
  command: (item: ReferenceOption) => void;
}

/**
 * Suggestion popup body for the playbook `@` reference mention. One row per
 * matching reference with its namespace icon + short description.
 */
export const ReferenceMentionList = forwardRef<ReferenceMentionListRef, ReferenceMentionListProps>(
  ({ items, command }, ref) => {
    const [selectedIndex, setSelectedIndex] = useState(0);
    const itemRefs = useRef<(HTMLButtonElement | null)[]>([]);

    useEffect(() => {
      setSelectedIndex(0);
    }, [items]);

    useEffect(() => {
      itemRefs.current[selectedIndex]?.scrollIntoView({ block: 'nearest' });
    }, [selectedIndex]);

    useImperativeHandle(ref, () => ({
      onKeyDown: ({ event }) => {
        if (event.key === 'ArrowUp') {
          setSelectedIndex((i) => (i - 1 + Math.max(items.length, 1)) % Math.max(items.length, 1));
          return true;
        }
        if (event.key === 'ArrowDown') {
          setSelectedIndex((i) => (i + 1) % Math.max(items.length, 1));
          return true;
        }
        if (event.key === 'Enter') {
          const item = items[selectedIndex];
          if (item) {
            command(item);
            return true;
          }
          return false;
        }
        return false;
      },
    }));

    if (items.length === 0) {
      return (
        <div className="text-muted-foreground z-99999 w-72 rounded-xl border border-[#E7E7E7] bg-white p-2 text-xs shadow-lg dark:border-[#252525] dark:bg-[#1A1A1A]">
          No references found
        </div>
      );
    }

    return (
      <div className="z-99999 max-h-72 w-72 overflow-y-auto rounded-xl border border-[#E7E7E7] bg-white p-1 shadow-lg dark:border-[#252525] dark:bg-[#1A1A1A]">
        {items.map((item, index) => {
          const meta = referenceMeta(item.path);
          const Icon = meta.icon;
          return (
            <button
              key=[redacted]
              ref={(el) => {
                itemRefs.current[index] = el;
              }}
              onMouseDown={(e) => e.preventDefault()}
              onClick={() => command(item)}
              className={cn(
                'flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left transition-colors',
                index === selectedIndex
                  ? 'bg-gray-100 dark:bg-[#252525]'
                  : 'hover:bg-gray-50 dark:hover:bg-[#1E1E1E]',
              )}
            >
              <span
                className={cn(
                  'flex h-5 w-5 shrink-0 items-center justify-center rounded-full',
                  meta.className,
                )}
              >
                <Icon className="h-3 w-3" />
              </span>
              <span className="flex min-w-0 flex-col">
                <span className="truncate text-sm">{referenceLabel(item.path)}</span>
                <span className="text-muted-foreground truncate text-xs">{item.description}</span>
              </span>
            </button>
          );
        })}
      </div>
    );
  },
);

ReferenceMentionList.displayName = 'ReferenceMentionList';