Skip to content

Commit 276e78e

Browse files
authored
Merge pull request #7 from typelets/feature/editor-ui-enhancements
feat: enhance editor with UI components and fix usage display
2 parents d117973 + 2502db7 commit 276e78e

6 files changed

Lines changed: 287 additions & 53 deletions

File tree

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,15 @@
7070
"@radix-ui/react-tabs": "^1.1.13",
7171
"@tailwindcss/vite": "^4.1.11",
7272
"@tiptap/extension-code-block-lowlight": "^2.26.1",
73+
"@tiptap/extension-color": "^2.26.1",
7374
"@tiptap/extension-dropcursor": "^3.4.1",
7475
"@tiptap/extension-highlight": "^3.4.1",
7576
"@tiptap/extension-horizontal-rule": "^3.4.1",
7677
"@tiptap/extension-image": "^3.4.1",
7778
"@tiptap/extension-link": "^3.4.1",
7879
"@tiptap/extension-task-item": "^2.26.1",
7980
"@tiptap/extension-task-list": "^2.26.1",
81+
"@tiptap/extension-text-style": "^2.26.1",
8082
"@tiptap/extension-underline": "^3.1.0",
8183
"@tiptap/pm": "^2.26.1",
8284
"@tiptap/react": "^2.26.1",

pnpm-lock.yaml

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/editor/Editor/Toolbar.tsx

Lines changed: 232 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ import {
2020
Minus,
2121
Highlighter,
2222
FileText,
23+
Palette,
24+
RemoveFormatting,
25+
Copy,
26+
Smile,
2327
} from 'lucide-react';
2428

2529
import { 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
}

src/components/editor/config/editor-config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import Link from '@tiptap/extension-link';
66
import HorizontalRule from '@tiptap/extension-horizontal-rule';
77
import Highlight from '@tiptap/extension-highlight';
88
import { Dropcursor } from '@tiptap/extension-dropcursor';
9+
import Color from '@tiptap/extension-color';
10+
import TextStyle from '@tiptap/extension-text-style';
911
import { TableOfContents } from '../extensions/TableOfContents';
1012
import { ResizableImage } from '../extensions/ResizableImage';
1113
import StarterKit from '@tiptap/starter-kit';
@@ -51,6 +53,8 @@ export function createEditorExtensions() {
5153
levels: [1, 2, 3],
5254
},
5355
codeBlock: false,
56+
horizontalRule: false,
57+
dropcursor: false,
5458
}),
5559
CodeBlockLowlight.configure({
5660
lowlight,
@@ -97,5 +101,7 @@ export function createEditorExtensions() {
97101
color: '#6b7280',
98102
width: 2,
99103
}),
104+
TextStyle,
105+
Color,
100106
];
101107
}

0 commit comments

Comments
 (0)