RichTextEditor.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. import { useEditor, EditorContent } from '@tiptap/react';
  2. import StarterKit from '@tiptap/starter-kit';
  3. import Link from '@tiptap/extension-link';
  4. import Underline from '@tiptap/extension-underline';
  5. import TextAlign from '@tiptap/extension-text-align';
  6. import { TextStyle } from '@tiptap/extension-text-style';
  7. import Color from '@tiptap/extension-color';
  8. import Image from '@tiptap/extension-image';
  9. import {
  10. Bold,
  11. Italic,
  12. Underline as UnderlineIcon,
  13. List,
  14. ListOrdered,
  15. AlignLeft,
  16. AlignCenter,
  17. AlignRight,
  18. Link as LinkIcon,
  19. Unlink,
  20. } from 'lucide-react';
  21. interface RichTextEditorProps {
  22. content: string;
  23. onChange: (html: string) => void;
  24. placeholder?: string;
  25. }
  26. export function RichTextEditor({ content, onChange, placeholder }: RichTextEditorProps) {
  27. const editor = useEditor({
  28. extensions: [
  29. StarterKit.configure({
  30. heading: false,
  31. codeBlock: false,
  32. code: false,
  33. }),
  34. Underline,
  35. Link.configure({
  36. openOnClick: false,
  37. HTMLAttributes: {
  38. target: '_blank',
  39. rel: 'noopener noreferrer',
  40. },
  41. }),
  42. TextAlign.configure({
  43. types: ['paragraph'],
  44. }),
  45. TextStyle,
  46. Color,
  47. Image.configure({
  48. HTMLAttributes: {
  49. style: 'max-width: 100%; height: auto;',
  50. },
  51. }),
  52. ],
  53. content,
  54. onUpdate: ({ editor }) => {
  55. onChange(editor.getHTML());
  56. },
  57. editorProps: {
  58. attributes: {
  59. class: 'prose prose-invert prose-sm max-w-none focus:outline-none min-h-[120px] px-3 py-2',
  60. placeholder: placeholder || '',
  61. },
  62. },
  63. });
  64. if (!editor) {
  65. return null;
  66. }
  67. const ToolbarButton = ({
  68. onClick,
  69. isActive = false,
  70. children,
  71. title,
  72. }: {
  73. onClick: () => void;
  74. isActive?: boolean;
  75. children: React.ReactNode;
  76. title: string;
  77. }) => (
  78. <button
  79. type="button"
  80. onClick={onClick}
  81. title={title}
  82. className={`p-1.5 rounded hover:bg-bambu-dark-tertiary transition-colors ${
  83. isActive ? 'bg-bambu-dark-tertiary text-bambu-green' : 'text-bambu-gray'
  84. }`}
  85. >
  86. {children}
  87. </button>
  88. );
  89. const setLink = () => {
  90. const url = window.prompt('Enter URL:');
  91. if (url) {
  92. editor.chain().focus().setLink({ href: url }).run();
  93. }
  94. };
  95. return (
  96. <div className="border border-bambu-dark-tertiary rounded-lg overflow-hidden bg-bambu-dark">
  97. {/* Toolbar */}
  98. <div className="flex items-center gap-0.5 p-1.5 border-b border-bambu-dark-tertiary bg-bambu-dark-secondary">
  99. <ToolbarButton
  100. onClick={() => editor.chain().focus().toggleBold().run()}
  101. isActive={editor.isActive('bold')}
  102. title="Bold"
  103. >
  104. <Bold className="w-4 h-4" />
  105. </ToolbarButton>
  106. <ToolbarButton
  107. onClick={() => editor.chain().focus().toggleItalic().run()}
  108. isActive={editor.isActive('italic')}
  109. title="Italic"
  110. >
  111. <Italic className="w-4 h-4" />
  112. </ToolbarButton>
  113. <ToolbarButton
  114. onClick={() => editor.chain().focus().toggleUnderline().run()}
  115. isActive={editor.isActive('underline')}
  116. title="Underline"
  117. >
  118. <UnderlineIcon className="w-4 h-4" />
  119. </ToolbarButton>
  120. <div className="w-px h-5 bg-bambu-dark-tertiary mx-1" />
  121. <ToolbarButton
  122. onClick={() => editor.chain().focus().toggleBulletList().run()}
  123. isActive={editor.isActive('bulletList')}
  124. title="Bullet List"
  125. >
  126. <List className="w-4 h-4" />
  127. </ToolbarButton>
  128. <ToolbarButton
  129. onClick={() => editor.chain().focus().toggleOrderedList().run()}
  130. isActive={editor.isActive('orderedList')}
  131. title="Numbered List"
  132. >
  133. <ListOrdered className="w-4 h-4" />
  134. </ToolbarButton>
  135. <div className="w-px h-5 bg-bambu-dark-tertiary mx-1" />
  136. <ToolbarButton
  137. onClick={() => editor.chain().focus().setTextAlign('left').run()}
  138. isActive={editor.isActive({ textAlign: 'left' })}
  139. title="Align Left"
  140. >
  141. <AlignLeft className="w-4 h-4" />
  142. </ToolbarButton>
  143. <ToolbarButton
  144. onClick={() => editor.chain().focus().setTextAlign('center').run()}
  145. isActive={editor.isActive({ textAlign: 'center' })}
  146. title="Align Center"
  147. >
  148. <AlignCenter className="w-4 h-4" />
  149. </ToolbarButton>
  150. <ToolbarButton
  151. onClick={() => editor.chain().focus().setTextAlign('right').run()}
  152. isActive={editor.isActive({ textAlign: 'right' })}
  153. title="Align Right"
  154. >
  155. <AlignRight className="w-4 h-4" />
  156. </ToolbarButton>
  157. <div className="w-px h-5 bg-bambu-dark-tertiary mx-1" />
  158. <ToolbarButton
  159. onClick={setLink}
  160. isActive={editor.isActive('link')}
  161. title="Add Link"
  162. >
  163. <LinkIcon className="w-4 h-4" />
  164. </ToolbarButton>
  165. {editor.isActive('link') && (
  166. <ToolbarButton
  167. onClick={() => editor.chain().focus().unsetLink().run()}
  168. title="Remove Link"
  169. >
  170. <Unlink className="w-4 h-4" />
  171. </ToolbarButton>
  172. )}
  173. </div>
  174. {/* Editor */}
  175. <EditorContent editor={editor} />
  176. </div>
  177. );
  178. }