EventChip.tsx2.3 KBView on GitHub
'use client';

/**
 * The event chip, extracted out of `EventNodeView` so the prose editor and the table grid
 * render literally the same component (apps/mail/docs/table-documents.md §3.2 step 10).
 * Same `container` mechanism as `ConversationChipContent` — read that module's comment for
 * why the in-editor mount must BE the `NodeViewWrapper`.
 *
 * An event chip carries a denormalized `title` snapshot rather than resolving live: the
 * mention popover writes the title into the node's attrs, so there is nothing to fetch.
 * A table cell holding `[[event: id]]` has no snapshot, which is why `title` is optional
 * and falls back to the id — an honest "this is an event, we don't know which" rather than
 * a fabricated label.
 */

import { createElement, type ElementType, type ReactNode } from 'react';
import {
  Calendar,
  FileQuestion,
  Mail,
  MessageSquare,
  Phone,
  Sparkles,
  StickyNote,
} from 'lucide-react';

import { cn } from '@/lib/utils';
import { REF_CHIP_CLASS } from '@/modules/documents/chip-styles';

const ICON_BY_TYPE: Record<string, React.ComponentType<{ className?: string }>> = {
  email: Mail,
  slack: MessageSquare,
  meeting: Calendar,
  call: Phone,
  note: StickyNote,
  custom: Sparkles,
};

export interface EventChipContentProps {
  eventType?: string | null;
  title?: string | null;
  className?: string;
  /** Element the chip renders AS. The PM node view passes `NodeViewWrapper`. */
  container?: ElementType;
  /** Forwarded verbatim to the container: PM's data attributes, `contentEditable`, `as`. */
  containerProps?: Record<string, unknown>;
}

export function EventChipContent({
  eventType,
  title,
  className,
  container,
  containerProps,
}: EventChipContentProps) {
  const Icon = ICON_BY_TYPE[eventType ?? ''] ?? FileQuestion;
  const label = title?.trim() || 'Event';
  const shared = { title: label, className: cn(REF_CHIP_CLASS, 'font-medium', className) };

  const body: ReactNode = (
    <>
      <Icon className="text-muted-foreground h-3 w-3 shrink-0" />
      <span className="truncate">{label}</span>
    </>
  );

  // See `ConversationChipContent` for why the polymorphic branch uses `createElement`.
  if (container) return createElement(container, { ...containerProps, ...shared }, body);
  return <span {...shared}>{body}</span>;
}