import { useEffect, useRef } from 'react';
import { FileIcon } from '@renderer/components/team/editor/FileIcon';
import { MemberBadge } from '@renderer/components/team/MemberBadge';
import { getTeamColorSet, getThemedText } from '@renderer/constants/teamColors';
import { useTheme } from '@renderer/hooks/useTheme';
import { nameColorSet } from '@renderer/utils/projectColor';
import { Command, Folder, Hash, Loader2, UsersRound } from 'lucide-react';
import type { MentionSuggestion } from '@renderer/types/mention';
interface MentionSuggestionListProps {
suggestions: MentionSuggestion[];
selectedIndex: number;
onSelect: (s: MentionSuggestion) => void;
query: string;
/** When true, adjusts empty state text to mention files */
hasFileSearch?: boolean;
/** When true, shows a loading spinner for file search */
filesLoading?: boolean;
}
const HighlightedName = ({ name, query }: { name: string; query: string }): React.JSX.Element => {
if (!query) return {name};
const lower = name.toLowerCase();
const qLower = query.toLowerCase();
const idx = lower.indexOf(qLower);
if (idx < 0) return {name};
const before = name.slice(0, idx);
const match = name.slice(idx, idx + query.length);
const after = name.slice(idx + query.length);
return (
{before}
{match}
{after}
);
};
/** Section header for grouped suggestion lists */
const SectionHeader = ({ label }: { label: string }): React.JSX.Element => (
{label}
);
export const MentionSuggestionList = ({
suggestions,
selectedIndex,
onSelect,
query,
hasFileSearch,
filesLoading,
}: MentionSuggestionListProps): React.JSX.Element => {
const listRef = useRef(null);
const { isLight } = useTheme();
useEffect(() => {
const list = listRef.current;
if (!list) return;
// Query by role=option to skip section headers
const options = list.querySelectorAll('[role="option"]');
const selected = options[selectedIndex] as HTMLElement | undefined;
selected?.scrollIntoView({ block: 'nearest' });
}, [selectedIndex]);
if (suggestions.length === 0) {
const emptyStateText = filesLoading
? 'Searching...'
: hasFileSearch
? 'No matching suggestions'
: 'No matching suggestions';
return (
{emptyStateText}
);
}
// Categorize suggestions (folders are grouped with files)
type Section = 'member' | 'team' | 'task' | 'file' | 'command';
const getSuggestionSection = (s: MentionSuggestion): Section => {
if (s.type === 'file' || s.type === 'folder') return 'file';
if (s.type === 'task') return 'task';
if (s.type === 'command') return 'command';
if (s.type === 'team') return 'team';
return 'member';
};
const sectionLabel: Record = {
member: 'Members',
team: 'Teams',
task: 'Tasks',
file: 'Files',
command: 'Commands',
};
// Determine which sections are present
const presentSections = new Set(suggestions.map(getSuggestionSection));
const showSections = presentSections.size > 1;
// Build items with section headers inserted
const items: React.JSX.Element[] = [];
let currentSection: Section | null = null;
let optionIndex = 0;
for (const s of suggestions) {
const section = getSuggestionSection(s);
const isFile = s.type === 'file';
const isFolder = s.type === 'folder';
const isFileOrFolder = isFile || isFolder;
const isTeam = section === 'team';
const isTask = section === 'task';
const isCommand = section === 'command';
const taskTeamColorSet =
isTask && s.color
? getTeamColorSet(s.color)
: isTask && s.teamDisplayName
? nameColorSet(s.teamDisplayName, isLight)
: null;
// Insert section header on transition
if (showSections && section !== currentSection) {
items.push();
currentSection = section;
}
const isSelected = optionIndex === selectedIndex;
const colorSet = isFileOrFolder
? null
: s.color
? getTeamColorSet(s.color)
: isTeam
? nameColorSet(s.name)
: null;
const idx = optionIndex;
optionIndex++;
items.push(
{
e.preventDefault();
onSelect(s);
}}
>
{isFolder ? (
) : isFile ? (
) : isTask ? (
) : isCommand ? (
) : isTeam ? (
) : (
)}
{!isTask && !isFileOrFolder && s.subtitle ? (
{s.subtitle}
) : null}
{isTask && s.ownerName ? (
) : null}
{isTask && s.teamDisplayName ? (
{s.teamDisplayName}
) : null}
{isTask && s.subtitle ? (
{s.subtitle}
) : null}
{isCommand && s.description ? (
{s.description}
) : null}
{isTeam && s.isOnline !== undefined ? (
) : null}
{s.subtitle && isFileOrFolder ? (
{'\u200E' + s.subtitle}
) : null}
);
}
return (
{items}
{filesLoading ? (
-
Searching files...
) : null}
);
};