import { type ReactElement, useMemo, useState } from "react"; import { Braces, Search } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { cn } from "@/lib/utils"; /** * MergeTagReference — WealthX Design System * * A popover that lists the personalisation tokens ("merge tags") available in * an email template, grouped by source (Contact / Company / Broker), each with * a human label and a description of what it resolves to. Answers the common * "where is the list of merge tags and what do they mean?" question, and lets * the author drop a token in via `onSelect`. * * Pure/presentational: the caller supplies the groups and decides what * `onSelect(token)` does (insert into the body, copy to clipboard, etc.). */ export interface MergeTagReferenceItem { /** The literal token, e.g. "{{contact.first_name}}". */ token: string; /** Human label, e.g. "First name". */ label: string; /** What the token resolves to when the email is sent. */ description: string; } export interface MergeTagReferenceGroup { /** Group heading, e.g. "Contact". */ group: string; tags: MergeTagReferenceItem[]; } export interface MergeTagReferenceProps { /** The merge tags to list, grouped by source. */ groups: MergeTagReferenceGroup[]; /** Called with the token when a tag is chosen (closes the popover). */ onSelect?: (token: string) => void; /** Trigger button label. */ triggerLabel?: string; /** Popover alignment against the trigger. */ align?: "start" | "center" | "end"; className?: string; } /** Popover reference of a template's available merge tags, grouped by source. */ export function MergeTagReference({ groups, onSelect, triggerLabel = "Merge tags", align = "end", className, }: MergeTagReferenceProps): ReactElement { const [open, setOpen] = useState(false); const [query, setQuery] = useState(""); const filtered = useMemo(() => { const q = query.trim().toLowerCase(); if (!q) return groups; return groups .map((g) => ({ ...g, tags: g.tags.filter( (t) => t.label.toLowerCase().includes(q) || t.token.toLowerCase().includes(q) || t.description.toLowerCase().includes(q), ), })) .filter((g) => g.tags.length > 0); }, [groups, query]); const handleSelect = (token: string) => { onSelect?.(token); setOpen(false); setQuery(""); }; return ( {triggerLabel} } />
setQuery(e.target.value)} placeholder="Search merge tags…" className="h-8 pl-8" />
{filtered.length === 0 ? (

No merge tags match "{query}".

) : ( filtered.map((g) => (

{g.group}

{g.tags.map((t) => ( ))}
)) )}
); } export default MergeTagReference;