'use client' import { Node, mergeAttributes } from '@tiptap/core' import { ReactNodeViewRenderer, NodeViewWrapper, NodeViewContent } from '@tiptap/react' import { Info, AlertTriangle, Lightbulb, CheckCircle, XCircle, Trash2, X } from 'lucide-react' import { useState } from 'react' import { cn } from '@/lib/utils' import { useLanguage } from '@/lib/i18n' type CalloutType = 'info' | 'warning' | 'tip' | 'success' | 'danger' const CALLOUT_STYLES: Record = { info: { bg: 'bg-blue-50 dark:bg-blue-950/30', border: 'border-blue-300 dark:border-blue-800', icon: 'text-blue-500', iconColor: 'text-blue-500' }, warning: { bg: 'bg-amber-50 dark:bg-amber-950/30', border: 'border-amber-300 dark:border-amber-800', icon: 'text-amber-500', iconColor: 'text-amber-500' }, tip: { bg: 'bg-purple-50 dark:bg-purple-950/30', border: 'border-purple-300 dark:border-purple-800', icon: 'text-purple-500', iconColor: 'text-purple-500' }, success: { bg: 'bg-green-50 dark:bg-green-950/30', border: 'border-green-300 dark:border-green-800', icon: 'text-green-500', iconColor: 'text-green-500' }, danger: { bg: 'bg-red-50 dark:bg-red-950/30', border: 'border-red-300 dark:border-red-800', icon: 'text-red-500', iconColor: 'text-red-500' }, } const CALLOUT_ICONS: Record = { info: Info, warning: AlertTriangle, tip: Lightbulb, success: CheckCircle, danger: XCircle, } const CALLOUT_TYPES: CalloutType[] = ['info', 'warning', 'tip', 'success', 'danger'] function CalloutTypePicker({ current, onChange }: { current: CalloutType; onChange: (t: CalloutType) => void }) { const [open, setOpen] = useState(false) const CurrentIcon = CALLOUT_ICONS[current] return (
{open && ( <>
setOpen(false)} />
{CALLOUT_TYPES.map(t => { const Icon = CALLOUT_ICONS[t] return ( ) })}
)}
) } const CalloutView = ({ node, updateAttributes, deleteNode, getPos, editor }: any) => { const type = (node.attrs.type || 'info') as CalloutType const styles = CALLOUT_STYLES[type] const { t } = useLanguage() const unwrap = () => { const pos = getPos() if (typeof pos !== 'number') return const toggleNode = editor.state.doc.nodeAt(pos) if (!toggleNode) return const children: any[] = [] toggleNode.forEach((child: any) => { children.push(child.toJSON()) }) if (children.length === 0) children.push({ type: 'paragraph' }) editor.chain().focus().deleteRange({ from: pos, to: pos + toggleNode.nodeSize }).run() editor.chain().focus().insertContentAt(pos, children).run() } return (
updateAttributes({ type: newType })} />
) } export const CalloutExtension = Node.create({ name: 'calloutBlock', group: 'block', content: 'block+', defining: true, isolating: true, addAttributes() { return { type: { default: 'info', parseHTML: (element) => { const t = element.getAttribute('data-callout-type') return (t && CALLOUT_TYPES.includes(t as CalloutType)) ? t : 'info' }, renderHTML: (attributes) => ({ 'data-callout-type': attributes.type, }), }, } }, parseHTML() { return [ { tag: 'div[data-type="callout-block"]' }, { tag: 'div[data-callout-type]' }, ] }, renderHTML({ HTMLAttributes }) { const type = (HTMLAttributes['data-callout-type'] as CalloutType) || 'info' return [ 'div', mergeAttributes(HTMLAttributes, { 'data-type': 'callout-block', 'data-callout-type': type, class: `callout-block callout-${type}`, }), 0, ] }, addNodeView() { return ReactNodeViewRenderer(CalloutView) }, }) export function insertCalloutBlock(editor: any, type: CalloutType = 'info') { editor.chain().focus().insertContent({ type: 'calloutBlock', attrs: { type }, content: [{ type: 'paragraph' }], }).run() } export function turnIntoCalloutBlock(editor: any, blockPos: number, blockNode: any, type: CalloutType = 'info') { if (!blockNode || blockPos < 0) return const nodeJson = blockNode.toJSON() editor.chain().focus().deleteRange({ from: blockPos, to: blockPos + blockNode.nodeSize, }).insertContentAt(blockPos, { type: 'calloutBlock', attrs: { type }, content: [nodeJson], }).setTextSelection(blockPos + 2).run() } export { CALLOUT_TYPES, CALLOUT_ICONS, CALLOUT_STYLES } export type { CalloutType }