@@ -20,6 +20,10 @@ import {
2020 Minus ,
2121 Highlighter ,
2222 FileText ,
23+ Palette ,
24+ RemoveFormatting ,
25+ Copy ,
26+ Smile ,
2327} from 'lucide-react' ;
2428
2529import { Button } from '@/components/ui/button.tsx' ;
@@ -164,59 +168,172 @@ export function Toolbar({ editor }: ToolbarProps) {
164168 < Highlighter className = "h-4 w-4" />
165169 </ Button >
166170
171+ < DropdownMenu >
172+ < DropdownMenuTrigger asChild >
173+ < Button
174+ variant = "ghost"
175+ size = "sm"
176+ title = "Text Color"
177+ >
178+ < Palette className = "h-4 w-4" />
179+ < ChevronDown className = "ml-1 h-3 w-3" />
180+ </ Button >
181+ </ DropdownMenuTrigger >
182+ < DropdownMenuContent align = "start" className = "grid grid-cols-5 gap-1 p-2" >
183+ { [
184+ { label : 'Default' , color : null } ,
185+ { label : 'Red' , color : '#ef4444' } ,
186+ { label : 'Orange' , color : '#f97316' } ,
187+ { label : 'Yellow' , color : '#eab308' } ,
188+ { label : 'Green' , color : '#22c55e' } ,
189+ { label : 'Blue' , color : '#3b82f6' } ,
190+ { label : 'Indigo' , color : '#6366f1' } ,
191+ { label : 'Purple' , color : '#a855f7' } ,
192+ { label : 'Pink' , color : '#ec4899' } ,
193+ { label : 'Gray' , color : '#6b7280' } ,
194+ ] . map ( ( item ) => (
195+ < button
196+ key = { item . label }
197+ onClick = { ( ) => {
198+ if ( item . color ) {
199+ editor . chain ( ) . focus ( ) . setColor ( item . color ) . run ( ) ;
200+ } else {
201+ editor . chain ( ) . focus ( ) . unsetColor ( ) . run ( ) ;
202+ }
203+ } }
204+ className = "h-6 w-6 rounded border border-gray-300 hover:scale-110 transition-transform flex items-center justify-center"
205+ style = { { backgroundColor : item . color || 'transparent' } }
206+ title = { item . label }
207+ >
208+ { ! item . color && (
209+ < span className = "text-xs leading-none" > ✕</ span >
210+ ) }
211+ </ button >
212+ ) ) }
213+ </ DropdownMenuContent >
214+ </ DropdownMenu >
215+
167216 < div className = "bg-border mx-1 h-6 w-px" />
168217
169- < Button
170- variant = { editor . isActive ( 'heading' , { level : 1 } ) ? 'default' : 'ghost' }
171- size = "sm"
172- onClick = { ( ) => editor . chain ( ) . focus ( ) . toggleHeading ( { level : 1 } ) . run ( ) }
173- title = "Heading 1"
174- >
175- < Heading1 className = "h-4 w-4" />
176- </ Button >
177- < Button
178- variant = { editor . isActive ( 'heading' , { level : 2 } ) ? 'default' : 'ghost' }
179- size = "sm"
180- onClick = { ( ) => editor . chain ( ) . focus ( ) . toggleHeading ( { level : 2 } ) . run ( ) }
181- title = "Heading 2"
182- >
183- < Heading2 className = "h-4 w-4" />
184- </ Button >
185- < Button
186- variant = { editor . isActive ( 'heading' , { level : 3 } ) ? 'default' : 'ghost' }
187- size = "sm"
188- onClick = { ( ) => editor . chain ( ) . focus ( ) . toggleHeading ( { level : 3 } ) . run ( ) }
189- title = "Heading 3"
190- >
191- < Heading3 className = "h-4 w-4" />
192- </ Button >
218+ < DropdownMenu >
219+ < DropdownMenuTrigger asChild >
220+ < Button
221+ variant = { editor . isActive ( 'heading' ) ? 'default' : 'ghost' }
222+ size = "sm"
223+ className = "gap-1"
224+ title = "Headings"
225+ >
226+ { editor . isActive ( 'heading' , { level : 1 } ) ? (
227+ < Heading1 className = "h-4 w-4" />
228+ ) : editor . isActive ( 'heading' , { level : 2 } ) ? (
229+ < Heading2 className = "h-4 w-4" />
230+ ) : editor . isActive ( 'heading' , { level : 3 } ) ? (
231+ < Heading3 className = "h-4 w-4" />
232+ ) : (
233+ < Heading1 className = "h-4 w-4" />
234+ ) }
235+ < ChevronDown className = "h-3 w-3" />
236+ </ Button >
237+ </ DropdownMenuTrigger >
238+ < DropdownMenuContent align = "start" className = "w-40 p-1" >
239+ < DropdownMenuItem
240+ onClick = { ( ) => editor . chain ( ) . focus ( ) . setParagraph ( ) . run ( ) }
241+ className = { `mb-1 ${ ! editor . isActive ( 'heading' ) ? 'bg-accent' : '' } ` }
242+ >
243+ < span className = "text-sm" > Normal Text</ span >
244+ { ! editor . isActive ( 'heading' ) && (
245+ < span className = "ml-auto text-xs opacity-60" > ✓</ span >
246+ ) }
247+ </ DropdownMenuItem >
248+ < DropdownMenuSeparator className = "my-1" />
249+ < DropdownMenuItem
250+ onClick = { ( ) => editor . chain ( ) . focus ( ) . toggleHeading ( { level : 1 } ) . run ( ) }
251+ className = { `mb-1 ${ editor . isActive ( 'heading' , { level : 1 } ) ? 'bg-accent' : '' } ` }
252+ >
253+ < Heading1 className = "mr-2 h-4 w-4" />
254+ < span className = "text-sm font-semibold" > Heading 1</ span >
255+ { editor . isActive ( 'heading' , { level : 1 } ) && (
256+ < span className = "ml-auto text-xs opacity-60" > ✓</ span >
257+ ) }
258+ </ DropdownMenuItem >
259+ < DropdownMenuItem
260+ onClick = { ( ) => editor . chain ( ) . focus ( ) . toggleHeading ( { level : 2 } ) . run ( ) }
261+ className = { `mb-1 ${ editor . isActive ( 'heading' , { level : 2 } ) ? 'bg-accent' : '' } ` }
262+ >
263+ < Heading2 className = "mr-2 h-4 w-4" />
264+ < span className = "text-sm font-medium" > Heading 2</ span >
265+ { editor . isActive ( 'heading' , { level : 2 } ) && (
266+ < span className = "ml-auto text-xs opacity-60" > ✓</ span >
267+ ) }
268+ </ DropdownMenuItem >
269+ < DropdownMenuItem
270+ onClick = { ( ) => editor . chain ( ) . focus ( ) . toggleHeading ( { level : 3 } ) . run ( ) }
271+ className = { editor . isActive ( 'heading' , { level : 3 } ) ? 'bg-accent' : '' }
272+ >
273+ < Heading3 className = "mr-2 h-4 w-4" />
274+ < span className = "text-sm" > Heading 3</ span >
275+ { editor . isActive ( 'heading' , { level : 3 } ) && (
276+ < span className = "ml-auto text-xs opacity-60" > ✓</ span >
277+ ) }
278+ </ DropdownMenuItem >
279+ </ DropdownMenuContent >
280+ </ DropdownMenu >
193281
194282 < div className = "bg-border mx-1 h-6 w-px" />
195283
196- < Button
197- variant = { editor . isActive ( 'bulletList' ) ? 'default' : 'ghost' }
198- size = "sm"
199- onClick = { ( ) => editor . chain ( ) . focus ( ) . toggleBulletList ( ) . run ( ) }
200- title = "Bullet List"
201- >
202- < List className = "h-4 w-4" />
203- </ Button >
204- < Button
205- variant = { editor . isActive ( 'orderedList' ) ? 'default' : 'ghost' }
206- size = "sm"
207- onClick = { ( ) => editor . chain ( ) . focus ( ) . toggleOrderedList ( ) . run ( ) }
208- title = "Numbered List"
209- >
210- < ListOrdered className = "h-4 w-4" />
211- </ Button >
212- < Button
213- variant = { editor . isActive ( 'taskList' ) ? 'default' : 'ghost' }
214- size = "sm"
215- onClick = { ( ) => editor . chain ( ) . focus ( ) . toggleTaskList ( ) . run ( ) }
216- title = "Task List (Checkboxes)"
217- >
218- < CheckSquare className = "h-4 w-4" />
219- </ Button >
284+ < DropdownMenu >
285+ < DropdownMenuTrigger asChild >
286+ < Button
287+ variant = { editor . isActive ( 'bulletList' ) || editor . isActive ( 'orderedList' ) || editor . isActive ( 'taskList' ) ? 'default' : 'ghost' }
288+ size = "sm"
289+ className = "gap-1"
290+ title = "Lists"
291+ >
292+ { editor . isActive ( 'bulletList' ) ? (
293+ < List className = "h-4 w-4" />
294+ ) : editor . isActive ( 'orderedList' ) ? (
295+ < ListOrdered className = "h-4 w-4" />
296+ ) : editor . isActive ( 'taskList' ) ? (
297+ < CheckSquare className = "h-4 w-4" />
298+ ) : (
299+ < List className = "h-4 w-4" />
300+ ) }
301+ < ChevronDown className = "h-3 w-3" />
302+ </ Button >
303+ </ DropdownMenuTrigger >
304+ < DropdownMenuContent align = "start" className = "w-40 p-1" >
305+ < DropdownMenuItem
306+ onClick = { ( ) => editor . chain ( ) . focus ( ) . toggleBulletList ( ) . run ( ) }
307+ className = { `mb-1 ${ editor . isActive ( 'bulletList' ) ? 'bg-accent' : '' } ` }
308+ >
309+ < List className = "mr-2 h-4 w-4" />
310+ < span className = "text-sm" > Bullet List</ span >
311+ { editor . isActive ( 'bulletList' ) && (
312+ < span className = "ml-auto text-xs opacity-60" > ✓</ span >
313+ ) }
314+ </ DropdownMenuItem >
315+ < DropdownMenuItem
316+ onClick = { ( ) => editor . chain ( ) . focus ( ) . toggleOrderedList ( ) . run ( ) }
317+ className = { `mb-1 ${ editor . isActive ( 'orderedList' ) ? 'bg-accent' : '' } ` }
318+ >
319+ < ListOrdered className = "mr-2 h-4 w-4" />
320+ < span className = "text-sm" > Numbered List</ span >
321+ { editor . isActive ( 'orderedList' ) && (
322+ < span className = "ml-auto text-xs opacity-60" > ✓</ span >
323+ ) }
324+ </ DropdownMenuItem >
325+ < DropdownMenuItem
326+ onClick = { ( ) => editor . chain ( ) . focus ( ) . toggleTaskList ( ) . run ( ) }
327+ className = { editor . isActive ( 'taskList' ) ? 'bg-accent' : '' }
328+ >
329+ < CheckSquare className = "mr-2 h-4 w-4" />
330+ < span className = "text-sm" > Task List</ span >
331+ { editor . isActive ( 'taskList' ) && (
332+ < span className = "ml-auto text-xs opacity-60" > ✓</ span >
333+ ) }
334+ </ DropdownMenuItem >
335+ </ DropdownMenuContent >
336+ </ DropdownMenu >
220337
221338 < div className = "bg-border mx-1 h-6 w-px" />
222339
@@ -298,6 +415,73 @@ export function Toolbar({ editor }: ToolbarProps) {
298415 < Minus className = "h-4 w-4" />
299416 </ Button >
300417
418+ < div className = "bg-border mx-1 h-6 w-px" />
419+
420+ < Button
421+ variant = "ghost"
422+ size = "sm"
423+ onClick = { ( ) => editor . chain ( ) . focus ( ) . clearNodes ( ) . unsetAllMarks ( ) . run ( ) }
424+ title = "Clear Formatting"
425+ >
426+ < RemoveFormatting className = "h-4 w-4" />
427+ </ Button >
428+
429+ < Button
430+ variant = "ghost"
431+ size = "sm"
432+ onClick = { ( ) => {
433+ const content = editor . getText ( ) ;
434+ navigator . clipboard . writeText ( content ) ;
435+ } }
436+ title = "Copy to Clipboard"
437+ >
438+ < Copy className = "h-4 w-4" />
439+ </ Button >
440+
441+ < DropdownMenu >
442+ < DropdownMenuTrigger asChild >
443+ < Button
444+ variant = "ghost"
445+ size = "sm"
446+ title = "Insert Emoji"
447+ >
448+ < Smile className = "h-4 w-4" />
449+ </ Button >
450+ </ DropdownMenuTrigger >
451+ < DropdownMenuContent align = "end" className = "w-64 p-2" >
452+ < div className = "grid grid-cols-8 gap-1" >
453+ { [
454+ '😀' , '😃' , '😄' , '😁' , '😅' , '😂' , '🤣' , '😊' ,
455+ '😇' , '🙂' , '😉' , '😌' , '😍' , '🥰' , '😘' , '😗' ,
456+ '😙' , '😚' , '😋' , '😛' , '😜' , '🤪' , '😝' , '🤑' ,
457+ '🤗' , '🤭' , '🤫' , '🤔' , '🤐' , '🤨' , '😐' , '😑' ,
458+ '😶' , '😏' , '😒' , '🙄' , '😬' , '🤥' , '😺' , '😔' ,
459+ '😪' , '🤤' , '😴' , '😷' , '🤒' , '🤕' , '🤢' , '🤮' ,
460+ '🤧' , '🥵' , '🥶' , '😎' , '🤓' , '🧐' , '😕' , '😟' ,
461+ '🙁' , '☹️' , '😮' , '😯' , '😲' , '😳' , '🥺' , '😦' ,
462+ '😧' , '😨' , '😰' , '😥' , '😢' , '😭' , '😱' , '😖' ,
463+ '😣' , '😞' , '😓' , '😩' , '😫' , '🥱' , '😤' , '😡' ,
464+ '😠' , '🤬' , '😈' , '👿' , '💀' , '☠️' , '💩' , '🤡' ,
465+ '👍' , '👎' , '👌' , '✌️' , '🤞' , '🤟' , '🤘' , '🤙' ,
466+ '👏' , '🙌' , '👐' , '🤲' , '🙏' , '✍️' , '💪' , '🦾' ,
467+ '❤️' , '🧡' , '💛' , '💚' , '💙' , '💜' , '🖤' , '🤍' ,
468+ '🤎' , '💔' , '❣️' , '💕' , '💞' , '💓' , '💗' , '💖' ,
469+ '💘' , '💝' , '⭐' , '🌟' , '✨' , '⚡' , '🔥' , '💥' ,
470+ '🎉' , '🎊' , '🎈' , '🎁' , '🎯' , '🏆' , '🥇' , '🥈' ,
471+ ] . map ( ( emoji ) => (
472+ < button
473+ key = { emoji }
474+ onClick = { ( ) => editor . chain ( ) . focus ( ) . insertContent ( emoji ) . run ( ) }
475+ className = "hover:bg-gray-100 dark:hover:bg-gray-700 rounded p-1 text-xl"
476+ title = { `Insert ${ emoji } ` }
477+ >
478+ { emoji }
479+ </ button >
480+ ) ) }
481+ </ div >
482+ </ DropdownMenuContent >
483+ </ DropdownMenu >
484+
301485 </ div >
302486 ) ;
303487}
0 commit comments