"use client"; import { Node, mergeAttributes } from "@tiptap/react"; import { NodeViewContent, NodeViewWrapper, ReactNodeViewRenderer, type NodeViewProps, } from "@tiptap/react"; import { AlertTriangle, CheckCircle2, Info, XCircle, } from "lucide-react"; import * as React from "react"; import { cn } from "@/lib/utils"; export type CalloutType = "info" | "warning" | "success" | "error"; const TYPE_ICON: Record< CalloutType, React.ComponentType<{ className?: string }> > = { info: Info, warning: AlertTriangle, success: CheckCircle2, error: XCircle, }; const TYPE_STYLES: Record< CalloutType, { box: string; icon: string } > = { info: { box: "border border-blue-500/25 bg-blue-500/10 dark:bg-blue-500/15 dark:border-blue-400/30", icon: "text-blue-600 dark:text-blue-400", }, warning: { box: "border border-amber-500/30 bg-amber-500/10 dark:bg-amber-500/15 dark:border-amber-400/35", icon: "text-amber-700 dark:text-amber-400", }, success: { box: "border border-emerald-500/25 bg-emerald-500/10 dark:bg-emerald-500/15 dark:border-emerald-400/30", icon: "text-emerald-700 dark:text-emerald-400", }, error: { box: "border border-red-500/25 bg-red-500/10 dark:bg-red-500/15 dark:border-red-400/30", icon: "text-red-600 dark:text-red-400", }, }; const DEFAULT_EMOJI: Record = { info: "💡", warning: "⚠️", success: "✅", error: "⛔", }; function CalloutView(props: NodeViewProps) { const { node, updateAttributes, selected } = props; const type = (node.attrs.type as CalloutType) ?? "info"; const emoji = typeof node.attrs.emoji === "string" && node.attrs.emoji.length > 0 ? node.attrs.emoji : DEFAULT_EMOJI[type]; const Icon = TYPE_ICON[type]; const styles = TYPE_STYLES[type]; return (
{ const order: CalloutType[] = [ "info", "warning", "success", "error", ]; const next = order[(order.indexOf(type) + 1) % order.length]; updateAttributes({ type: next, emoji: DEFAULT_EMOJI[next], }); }} title="Cycle callout type" > {emoji}
); } export const Callout = Node.create({ name: "callout", group: "block", content: "block+", defining: true, addAttributes() { return { type: { default: "info", parseHTML: (el) => (el.getAttribute("data-callout-type") as CalloutType | null) ?? "info", renderHTML: (attrs) => ({ "data-callout-type": attrs.type ?? "info", }), }, emoji: { default: "", parseHTML: (el) => el.getAttribute("data-emoji") ?? "", renderHTML: (attrs) => attrs.emoji ? { "data-emoji": attrs.emoji } : {}, }, }; }, parseHTML() { return [ { tag: 'div[data-type="callout"]', }, ]; }, renderHTML({ HTMLAttributes }) { return [ "div", mergeAttributes(HTMLAttributes, { "data-type": "callout" }), 0, ]; }, addNodeView() { return ReactNodeViewRenderer(CalloutView); }, addKeyboardShortcuts() { return { "Mod-Shift-Alt-c": () => this.editor.commands.insertContent({ type: this.name, attrs: { type: "info", emoji: DEFAULT_EMOJI.info }, content: [{ type: "paragraph" }], }), }; }, });