ConversationMentionList.tsx3.5 KBView on GitHub 'use client';
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
import { Building2 } from 'lucide-react';
import { cn } from '@/lib/utils';
export interface ConversationMentionItem {
id: string;
name: string | null;
companyName: string | null;
companyLogoUrl: string | null;
}
interface ConversationMentionListProps {
items: ConversationMentionItem[];
command: (item: ConversationMentionItem) => void;
loading: boolean;
}
export interface ConversationMentionListRef {
onKeyDown: (data: { event: KeyboardEvent }) => boolean;
}
/**
* Suggestion popup body for the agenda `@` conversation mention.
* Renders one row per match — name only, no company sub-label, per design §9a.
*/
export const ConversationMentionList = forwardRef<
ConversationMentionListRef,
ConversationMentionListProps
>(({ items, command, loading }, 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 (loading && items.length === 0) {
return (
<div className="text-muted-foreground z-99999 w-64 rounded-xl border border-[#E7E7E7] bg-white p-2 text-xs shadow-lg dark:border-[#252525] dark:bg-[#1A1A1A]">
Searching…
</div>
);
}
if (items.length === 0) {
return (
<div className="text-muted-foreground z-99999 w-64 rounded-xl border border-[#E7E7E7] bg-white p-2 text-xs shadow-lg dark:border-[#252525] dark:bg-[#1A1A1A]">
No conversations found
</div>
);
}
return (
<div className="z-99999 max-h-64 w-64 overflow-y-auto rounded-xl border border-[#E7E7E7] bg-white p-1 shadow-lg dark:border-[#252525] dark:bg-[#1A1A1A]">
{items.map((item, index) => (
<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 text-sm transition-colors',
index === selectedIndex
? 'bg-gray-100 dark:bg-[#252525]'
: 'hover:bg-gray-50 dark:hover:bg-[#1E1E1E]',
)}
>
{item.companyLogoUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={item.companyLogoUrl}
alt=""
className="h-4 w-4 shrink-0 rounded-full object-contain"
/>
) : (
<Building2 className="text-muted-foreground h-4 w-4 shrink-0" />
)}
<span className="truncate">{item.name ?? item.companyName ?? 'Conversation'}</span>
</button>
))}
</div>
);
});
ConversationMentionList.displayName = 'ConversationMentionList';