| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191 |
- import { useEditor, EditorContent } from '@tiptap/react';
- import StarterKit from '@tiptap/starter-kit';
- import Link from '@tiptap/extension-link';
- import Underline from '@tiptap/extension-underline';
- import TextAlign from '@tiptap/extension-text-align';
- import { TextStyle } from '@tiptap/extension-text-style';
- import Color from '@tiptap/extension-color';
- import Image from '@tiptap/extension-image';
- import {
- Bold,
- Italic,
- Underline as UnderlineIcon,
- List,
- ListOrdered,
- AlignLeft,
- AlignCenter,
- AlignRight,
- Link as LinkIcon,
- Unlink,
- } from 'lucide-react';
- interface RichTextEditorProps {
- content: string;
- onChange: (html: string) => void;
- placeholder?: string;
- }
- export function RichTextEditor({ content, onChange, placeholder }: RichTextEditorProps) {
- const editor = useEditor({
- extensions: [
- StarterKit.configure({
- heading: false,
- codeBlock: false,
- code: false,
- }),
- Underline,
- Link.configure({
- openOnClick: false,
- HTMLAttributes: {
- target: '_blank',
- rel: 'noopener noreferrer',
- },
- }),
- TextAlign.configure({
- types: ['paragraph'],
- }),
- TextStyle,
- Color,
- Image.configure({
- HTMLAttributes: {
- style: 'max-width: 100%; height: auto;',
- },
- }),
- ],
- content,
- onUpdate: ({ editor }) => {
- onChange(editor.getHTML());
- },
- editorProps: {
- attributes: {
- class: 'prose prose-invert prose-sm max-w-none focus:outline-none min-h-[120px] px-3 py-2',
- placeholder: placeholder || '',
- },
- },
- });
- if (!editor) {
- return null;
- }
- const ToolbarButton = ({
- onClick,
- isActive = false,
- children,
- title,
- }: {
- onClick: () => void;
- isActive?: boolean;
- children: React.ReactNode;
- title: string;
- }) => (
- <button
- type="button"
- onClick={onClick}
- title={title}
- className={`p-1.5 rounded hover:bg-bambu-dark-tertiary transition-colors ${
- isActive ? 'bg-bambu-dark-tertiary text-bambu-green' : 'text-bambu-gray'
- }`}
- >
- {children}
- </button>
- );
- const setLink = () => {
- const url = window.prompt('Enter URL:');
- if (url) {
- editor.chain().focus().setLink({ href: url }).run();
- }
- };
- return (
- <div className="border border-bambu-dark-tertiary rounded-lg overflow-hidden bg-bambu-dark">
- {/* Toolbar */}
- <div className="flex items-center gap-0.5 p-1.5 border-b border-bambu-dark-tertiary bg-bambu-dark-secondary">
- <ToolbarButton
- onClick={() => editor.chain().focus().toggleBold().run()}
- isActive={editor.isActive('bold')}
- title="Bold"
- >
- <Bold className="w-4 h-4" />
- </ToolbarButton>
- <ToolbarButton
- onClick={() => editor.chain().focus().toggleItalic().run()}
- isActive={editor.isActive('italic')}
- title="Italic"
- >
- <Italic className="w-4 h-4" />
- </ToolbarButton>
- <ToolbarButton
- onClick={() => editor.chain().focus().toggleUnderline().run()}
- isActive={editor.isActive('underline')}
- title="Underline"
- >
- <UnderlineIcon className="w-4 h-4" />
- </ToolbarButton>
- <div className="w-px h-5 bg-bambu-dark-tertiary mx-1" />
- <ToolbarButton
- onClick={() => editor.chain().focus().toggleBulletList().run()}
- isActive={editor.isActive('bulletList')}
- title="Bullet List"
- >
- <List className="w-4 h-4" />
- </ToolbarButton>
- <ToolbarButton
- onClick={() => editor.chain().focus().toggleOrderedList().run()}
- isActive={editor.isActive('orderedList')}
- title="Numbered List"
- >
- <ListOrdered className="w-4 h-4" />
- </ToolbarButton>
- <div className="w-px h-5 bg-bambu-dark-tertiary mx-1" />
- <ToolbarButton
- onClick={() => editor.chain().focus().setTextAlign('left').run()}
- isActive={editor.isActive({ textAlign: 'left' })}
- title="Align Left"
- >
- <AlignLeft className="w-4 h-4" />
- </ToolbarButton>
- <ToolbarButton
- onClick={() => editor.chain().focus().setTextAlign('center').run()}
- isActive={editor.isActive({ textAlign: 'center' })}
- title="Align Center"
- >
- <AlignCenter className="w-4 h-4" />
- </ToolbarButton>
- <ToolbarButton
- onClick={() => editor.chain().focus().setTextAlign('right').run()}
- isActive={editor.isActive({ textAlign: 'right' })}
- title="Align Right"
- >
- <AlignRight className="w-4 h-4" />
- </ToolbarButton>
- <div className="w-px h-5 bg-bambu-dark-tertiary mx-1" />
- <ToolbarButton
- onClick={setLink}
- isActive={editor.isActive('link')}
- title="Add Link"
- >
- <LinkIcon className="w-4 h-4" />
- </ToolbarButton>
- {editor.isActive('link') && (
- <ToolbarButton
- onClick={() => editor.chain().focus().unsetLink().run()}
- title="Remove Link"
- >
- <Unlink className="w-4 h-4" />
- </ToolbarButton>
- )}
- </div>
- {/* Editor */}
- <EditorContent editor={editor} />
- </div>
- );
- }
|