"use client";
import React, { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import { ReactRenderer } from "@tiptap/react";
import tippy, { type Instance } from "tippy.js";
import { cn } from "@/lib/utils";
export interface Item { id: string; title: string; hint?: string; icon?: React.ReactNode; group?: string; run?: () => void; data?: any }
export const SuggestionList = forwardRef<{ onKeyDown: (p: { event: KeyboardEvent }) => boolean }, { items: Item[]; command: (item: Item) => void; empty?: string }>(({ items, command, empty = "No results" }, ref) => {
const [idx, setIdx] = useState(0);
useEffect(() => setIdx(0), [items]);
useImperativeHandle(ref, () => ({ onKeyDown: ({ event }) => {
if (event.key === "ArrowUp") { setIdx((i) => (i + items.length - 1) % Math.max(1, items.length)); return true; }
if (event.key === "ArrowDown") { setIdx((i) => (i + 1) % Math.max(1, items.length)); return true; }
if (event.key === "Enter") { if (items[idx]) command(items[idx]); return true; }
return false;
} }));
useEffect(() => { document.getElementById(`sg-${idx}`)?.scrollIntoView({ block: "nearest" }); }, [idx]);
if (!items.length) return
{empty}
;
let lastGroup: string | undefined;
return (
{items.map((it, i) => { const g = it.group !== lastGroup ? it.group : null; lastGroup = it.group; return (
{g && {g}
}
); })}
);
});
SuggestionList.displayName = "SuggestionList";
/** Generic Tiptap suggestion renderer (slash commands, mentions) using ReactRenderer + tippy. */
export function makeRenderer(Component: React.ComponentType, extra: Record = {}) {
return () => {
let component: ReactRenderer | null = null; let popup: Instance[] | null = null;
return {
onStart: (props: any) => {
component = new ReactRenderer(Component, { props: { ...props, ...extra }, editor: props.editor });
if (!props.clientRect) return;
popup = tippy("body", { getReferenceClientRect: props.clientRect, appendTo: () => document.body, content: component.element, showOnCreate: true, interactive: true, trigger: "manual", placement: "bottom-start", arrow: false, theme: "none", offset: [0, 6] });
},
onUpdate: (props: any) => { component?.updateProps({ ...props, ...extra }); if (props.clientRect) popup?.[0]?.setProps({ getReferenceClientRect: props.clientRect }); },
onKeyDown: (props: any) => { if (props.event.key === "Escape") { popup?.[0]?.hide(); return true; } return component?.ref?.onKeyDown?.(props) ?? false; },
onExit: () => { popup?.[0]?.destroy(); component?.destroy(); popup = null; component = null; },
};
};
}