diff --git a/src/components/documents/rich-text-editor.tsx b/src/components/documents/rich-text-editor.tsx new file mode 100644 index 0000000..330cd27 --- /dev/null +++ b/src/components/documents/rich-text-editor.tsx @@ -0,0 +1,149 @@ +import { useEditor, EditorContent, Editor } from "@tiptap/react"; +import StarterKit from "@tiptap/starter-kit"; +import Underline from "@tiptap/extension-underline"; +import TextAlign from "@tiptap/extension-text-align"; +import { useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Separator } from "@/components/ui/separator"; +import { + Bold, Italic, Underline as UnderlineIcon, List, ListOrdered, + AlignLeft, AlignCenter, AlignRight, AlignJustify, Quote, Undo, Redo, + Heading2, Superscript as SupIcon, +} from "lucide-react"; +import { cn } from "@/lib/utils"; + +interface RichTextEditorProps { + value: string; // HTML + onChange: (html: string) => void; + onInsertFootnote?: () => void; + className?: string; + minHeight?: number; +} + +function ToolbarButton({ + onClick, active, disabled, children, title, +}: { + onClick: () => void; active?: boolean; disabled?: boolean; children: React.ReactNode; title: string; +}) { + return ( + + ); +} + +export function RichTextEditor({ + value, onChange, onInsertFootnote, className, minHeight = 400, +}: RichTextEditorProps) { + const editor = useEditor({ + extensions: [ + StarterKit.configure({ heading: { levels: [2, 3] } }), + Underline, + TextAlign.configure({ types: ["heading", "paragraph"] }), + ], + content: value || "

", + onUpdate: ({ editor }) => onChange(editor.getHTML()), + editorProps: { + attributes: { + class: "prose prose-sm max-w-none focus:outline-none px-10 py-8 bg-white text-black", + style: 'font-family: "Bookman Old Style", "URW Bookman", Georgia, serif; font-size: 12pt; line-height: 1.5;', + }, + }, + }); + + // Keep editor in sync when value is reset externally + useEffect(() => { + if (editor && value !== editor.getHTML()) { + editor.commands.setContent(value || "

", { emitUpdate: false }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value === ""]); + + if (!editor) return null; + + return ( +
+ +
+
+ +
+
+
+ ); +} + +function Toolbar({ editor, onInsertFootnote }: { editor: Editor; onInsertFootnote?: () => void }) { + return ( +
+ editor.chain().focus().undo().run()} disabled={!editor.can().undo()}> + + + editor.chain().focus().redo().run()} disabled={!editor.can().redo()}> + + + + + editor.chain().focus().toggleBold().run()}> + + + editor.chain().focus().toggleItalic().run()}> + + + editor.chain().focus().toggleUnderline().run()}> + + + + + editor.chain().focus().toggleHeading({ level: 2 }).run()}> + + + editor.chain().focus().toggleBulletList().run()}> + + + editor.chain().focus().toggleOrderedList().run()}> + + + editor.chain().focus().toggleBlockquote().run()}> + + + + + editor.chain().focus().setTextAlign("left").run()}> + + + editor.chain().focus().setTextAlign("center").run()}> + + + editor.chain().focus().setTextAlign("right").run()}> + + + editor.chain().focus().setTextAlign("justify").run()}> + + + + {onInsertFootnote && ( + <> + + + + )} +
+ ); +} + +// Helper exposed for the parent to insert a numbered footnote marker +export function insertFootnoteMarker(editor: Editor | null, num: number) { + if (!editor) return; + editor.chain().focus().insertContent(`[${num}]`).run(); +}