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';