@moontra/moonui-pro 2.0.22 → 2.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/package.json +2 -1
  2. package/src/__tests__/use-intersection-observer.test.tsx +216 -0
  3. package/src/__tests__/use-local-storage.test.tsx +174 -0
  4. package/src/__tests__/use-pro-access.test.tsx +183 -0
  5. package/src/components/advanced-chart/advanced-chart.test.tsx +281 -0
  6. package/src/components/advanced-chart/index.tsx +412 -0
  7. package/src/components/advanced-forms/index.tsx +431 -0
  8. package/src/components/animated-button/index.tsx +202 -0
  9. package/src/components/calendar/event-dialog.tsx +372 -0
  10. package/src/components/calendar/index.tsx +531 -0
  11. package/src/components/color-picker/index.tsx +434 -0
  12. package/src/components/dashboard/index.tsx +334 -0
  13. package/src/components/data-table/data-table.test.tsx +187 -0
  14. package/src/components/data-table/index.tsx +368 -0
  15. package/src/components/draggable-list/index.tsx +100 -0
  16. package/src/components/enhanced/button.tsx +360 -0
  17. package/src/components/enhanced/card.tsx +272 -0
  18. package/src/components/enhanced/dialog.tsx +248 -0
  19. package/src/components/enhanced/index.ts +3 -0
  20. package/src/components/error-boundary/index.tsx +111 -0
  21. package/src/components/file-upload/file-upload.test.tsx +242 -0
  22. package/src/components/file-upload/index.tsx +362 -0
  23. package/src/components/floating-action-button/index.tsx +209 -0
  24. package/src/components/github-stars/index.tsx +414 -0
  25. package/src/components/health-check/index.tsx +441 -0
  26. package/src/components/hover-card-3d/index.tsx +170 -0
  27. package/src/components/index.ts +76 -0
  28. package/src/components/kanban/index.tsx +436 -0
  29. package/src/components/lazy-component/index.tsx +342 -0
  30. package/src/components/magnetic-button/index.tsx +170 -0
  31. package/src/components/memory-efficient-data/index.tsx +352 -0
  32. package/src/components/optimized-image/index.tsx +427 -0
  33. package/src/components/performance-debugger/index.tsx +591 -0
  34. package/src/components/performance-monitor/index.tsx +775 -0
  35. package/src/components/pinch-zoom/index.tsx +172 -0
  36. package/src/components/rich-text-editor/index-old-backup.tsx +443 -0
  37. package/src/components/rich-text-editor/index.tsx +1537 -0
  38. package/src/components/rich-text-editor/slash-commands-extension.ts +220 -0
  39. package/src/components/rich-text-editor/slash-commands.css +35 -0
  40. package/src/components/rich-text-editor/table-styles.css +65 -0
  41. package/src/components/spotlight-card/index.tsx +194 -0
  42. package/src/components/swipeable-card/index.tsx +100 -0
  43. package/src/components/timeline/index.tsx +333 -0
  44. package/src/components/ui/animated-button.tsx +185 -0
  45. package/src/components/ui/avatar.tsx +135 -0
  46. package/src/components/ui/badge.tsx +225 -0
  47. package/src/components/ui/button.tsx +221 -0
  48. package/src/components/ui/card.tsx +141 -0
  49. package/src/components/ui/checkbox.tsx +256 -0
  50. package/src/components/ui/color-picker.tsx +95 -0
  51. package/src/components/ui/dialog.tsx +332 -0
  52. package/src/components/ui/dropdown-menu.tsx +200 -0
  53. package/src/components/ui/hover-card-3d.tsx +103 -0
  54. package/src/components/ui/index.ts +33 -0
  55. package/src/components/ui/input.tsx +219 -0
  56. package/src/components/ui/label.tsx +26 -0
  57. package/src/components/ui/magnetic-button.tsx +129 -0
  58. package/src/components/ui/popover.tsx +183 -0
  59. package/src/components/ui/select.tsx +273 -0
  60. package/src/components/ui/separator.tsx +140 -0
  61. package/src/components/ui/slider.tsx +351 -0
  62. package/src/components/ui/spotlight-card.tsx +119 -0
  63. package/src/components/ui/switch.tsx +83 -0
  64. package/src/components/ui/tabs.tsx +195 -0
  65. package/src/components/ui/textarea.tsx +25 -0
  66. package/src/components/ui/toast.tsx +313 -0
  67. package/src/components/ui/tooltip.tsx +152 -0
  68. package/src/components/virtual-list/index.tsx +369 -0
  69. package/src/hooks/use-chart.ts +205 -0
  70. package/src/hooks/use-data-table.ts +182 -0
  71. package/src/hooks/use-docs-pro-access.ts +13 -0
  72. package/src/hooks/use-license-check.ts +65 -0
  73. package/src/hooks/use-subscription.ts +19 -0
  74. package/src/index.ts +11 -0
  75. package/src/lib/micro-interactions.ts +255 -0
  76. package/src/lib/utils.ts +6 -0
  77. package/src/patterns/login-form/index.tsx +276 -0
  78. package/src/patterns/login-form/types.ts +67 -0
  79. package/src/setupTests.ts +41 -0
  80. package/src/styles/design-system.css +365 -0
  81. package/src/styles/index.css +4 -0
  82. package/src/styles/tailwind.css +6 -0
  83. package/src/styles/tokens.css +453 -0
  84. package/src/types/moonui.d.ts +22 -0
  85. package/src/use-intersection-observer.tsx +154 -0
  86. package/src/use-local-storage.tsx +71 -0
  87. package/src/use-paddle.ts +138 -0
  88. package/src/use-performance-optimizer.ts +379 -0
  89. package/src/use-pro-access.ts +141 -0
  90. package/src/use-scroll-animation.ts +221 -0
  91. package/src/use-subscription.ts +37 -0
  92. package/src/use-toast.ts +32 -0
  93. package/src/utils/chart-helpers.ts +257 -0
  94. package/src/utils/cn.ts +69 -0
  95. package/src/utils/data-processing.ts +151 -0
  96. package/src/utils/license-validator.tsx +183 -0
@@ -0,0 +1,1537 @@
1
+ "use client";
2
+
3
+ import React, { useState } from 'react';
4
+ import { useEditor, EditorContent } from '@tiptap/react';
5
+ import StarterKit from '@tiptap/starter-kit';
6
+ import Placeholder from '@tiptap/extension-placeholder';
7
+ import TextAlign from '@tiptap/extension-text-align';
8
+ import Highlight from '@tiptap/extension-highlight';
9
+ import Link from '@tiptap/extension-link';
10
+ import Image from '@tiptap/extension-image';
11
+ import { Table } from '@tiptap/extension-table';
12
+ import TableRow from '@tiptap/extension-table-row';
13
+ import TableCell from '@tiptap/extension-table-cell';
14
+ import TableHeader from '@tiptap/extension-table-header';
15
+ import Color from '@tiptap/extension-color';
16
+ import { TextStyle } from '@tiptap/extension-text-style';
17
+ import UnderlineExtension from '@tiptap/extension-underline';
18
+ import Gapcursor from '@tiptap/extension-gapcursor';
19
+ import TaskList from '@tiptap/extension-task-list';
20
+ import TaskItem from '@tiptap/extension-task-item';
21
+ import Typography from '@tiptap/extension-typography';
22
+ import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
23
+ import { common, createLowlight } from 'lowlight';
24
+ import { SlashCommandsExtension } from './slash-commands-extension';
25
+
26
+ // Import pro access hooks
27
+ // Note: DocsProAccess should be handled by consuming application
28
+ import { useSubscription } from '../../hooks/use-subscription';
29
+ import { Card, CardContent } from '../ui/card';
30
+
31
+ const lowlight = createLowlight(common);
32
+ import {
33
+ Bold,
34
+ Italic,
35
+ Underline,
36
+ Strikethrough,
37
+ Heading1,
38
+ Heading2,
39
+ Heading3,
40
+ List,
41
+ ListOrdered,
42
+ Quote,
43
+ Code,
44
+ Link2,
45
+ Image as ImageIcon,
46
+ Table as TableIcon,
47
+ AlignLeft,
48
+ AlignCenter,
49
+ AlignRight,
50
+ AlignJustify,
51
+ Highlighter,
52
+ Type,
53
+ Wand2,
54
+ Settings,
55
+ Sparkles,
56
+ ChevronDown,
57
+ Plus,
58
+ RefreshCw,
59
+ Maximize,
60
+ FileText,
61
+ Languages,
62
+ Check,
63
+ CheckSquare,
64
+ Undo,
65
+ Redo,
66
+ Palette,
67
+ Eye,
68
+ Edit,
69
+ Lock
70
+ } from 'lucide-react';
71
+ import { cn } from '../../lib/utils';
72
+ import { Button } from '../ui/button';
73
+ import {
74
+ DropdownMenu,
75
+ DropdownMenuContent,
76
+ DropdownMenuItem,
77
+ DropdownMenuSeparator,
78
+ DropdownMenuTrigger,
79
+ } from '../ui/dropdown-menu';
80
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip';
81
+ import {
82
+ Dialog,
83
+ DialogContent,
84
+ DialogDescription,
85
+ DialogHeader,
86
+ DialogTitle,
87
+ DialogTrigger,
88
+ } from '../ui/dialog';
89
+ import { Input } from '../ui/input';
90
+ import { Label } from '../ui/label';
91
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
92
+ import { Separator } from '../ui/separator';
93
+ import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
94
+ import { ColorPicker } from '../ui/color-picker';
95
+ import { Slider } from '../ui/slider';
96
+ import { toast } from '../ui/toast';
97
+ // Note: AI providers should be handled by consuming application
98
+
99
+ // Type definitions for AI functionality
100
+ type AIProvider = 'openai' | 'anthropic' | 'gemini'
101
+
102
+ interface AISettingsType {
103
+ provider: AIProvider
104
+ apiKey?: string
105
+ model?: string
106
+ maxTokens?: number
107
+ }
108
+
109
+ interface SlashCommand {
110
+ title: string
111
+ description: string
112
+ command: string
113
+ action: () => void
114
+ }
115
+
116
+ // Mock AI provider function
117
+ const getAIProvider = (settings: AISettingsType) => {
118
+ return {
119
+ generateText: async (prompt: string) => {
120
+ // Mock implementation - consuming app should provide real AI integration
121
+ return 'AI functionality requires proper API configuration'
122
+ }
123
+ }
124
+ }
125
+ import './slash-commands.css';
126
+ import './table-styles.css';
127
+
128
+ interface RichTextEditorProps {
129
+ value?: string;
130
+ onChange?: (value: string) => void;
131
+ placeholder?: string;
132
+ className?: string;
133
+ height?: number | string;
134
+ features?: {
135
+ bold?: boolean;
136
+ italic?: boolean;
137
+ underline?: boolean;
138
+ strike?: boolean;
139
+ heading?: boolean;
140
+ lists?: boolean;
141
+ blockquote?: boolean;
142
+ code?: boolean;
143
+ link?: boolean;
144
+ image?: boolean;
145
+ table?: boolean;
146
+ align?: boolean;
147
+ color?: boolean;
148
+ ai?: boolean;
149
+ };
150
+ aiConfig?: {
151
+ provider?: 'openai' | 'claude' | 'gemini' | 'cohere';
152
+ apiKey?: string;
153
+ model?: string;
154
+ temperature?: number;
155
+ maxTokens?: number;
156
+ };
157
+ }
158
+
159
+
160
+ const EditorColorPicker = ({
161
+ onColorSelect,
162
+ currentColor = "#000000"
163
+ }: {
164
+ onColorSelect: (color: string) => void;
165
+ currentColor?: string;
166
+ }) => {
167
+ return (
168
+ <ColorPicker
169
+ value={currentColor}
170
+ onChange={onColorSelect}
171
+ showInput={true}
172
+ showPresets={true}
173
+ size="sm"
174
+ presets={[
175
+ '#000000', '#ffffff', '#ff0000', '#00ff00', '#0000ff',
176
+ '#ffff00', '#ff00ff', '#00ffff', '#ff8800', '#8800ff',
177
+ '#88ff00', '#0088ff', '#ff0088', '#00ff88', '#888888',
178
+ '#f87171', '#fb923c', '#fbbf24', '#facc15', '#a3e635',
179
+ '#4ade80', '#34d399', '#2dd4bf', '#22d3ee', '#38bdf8',
180
+ '#60a5fa', '#818cf8', '#a78bfa', '#c084fc', '#e879f9',
181
+ '#f472b6', '#fb7185', '#f43f5e', '#ef4444', '#dc2626',
182
+ ]}
183
+ />
184
+ );
185
+ };
186
+
187
+ const ToolbarButton = ({
188
+ active,
189
+ disabled,
190
+ onClick,
191
+ children,
192
+ tooltip
193
+ }: {
194
+ active?: boolean;
195
+ disabled?: boolean;
196
+ onClick: () => void;
197
+ children: React.ReactNode;
198
+ tooltip?: string;
199
+ }) => {
200
+ const button = (
201
+ <button
202
+ type="button"
203
+ onClick={onClick}
204
+ disabled={disabled}
205
+ className={cn(
206
+ "p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors",
207
+ active && "bg-gray-100 dark:bg-gray-800",
208
+ disabled && "opacity-50 cursor-not-allowed"
209
+ )}
210
+ >
211
+ {children}
212
+ </button>
213
+ );
214
+
215
+ if (!tooltip) return button;
216
+
217
+ return (
218
+ <Tooltip>
219
+ <TooltipTrigger asChild>
220
+ {button}
221
+ </TooltipTrigger>
222
+ <TooltipContent>
223
+ <p>{tooltip}</p>
224
+ </TooltipContent>
225
+ </Tooltip>
226
+ );
227
+ };
228
+
229
+ export function RichTextEditor({
230
+ placeholder = 'Start writing...',
231
+ className,
232
+ height = 300,
233
+ features = {
234
+ bold: true,
235
+ italic: true,
236
+ underline: true,
237
+ strike: true,
238
+ heading: true,
239
+ lists: true,
240
+ blockquote: true,
241
+ code: true,
242
+ link: true,
243
+ image: true,
244
+ table: true,
245
+ align: true,
246
+ color: true,
247
+ ai: true,
248
+ },
249
+ aiConfig = {
250
+ provider: 'openai',
251
+ apiKey: '',
252
+ model: 'gpt-4',
253
+ temperature: 0.7,
254
+ maxTokens: 1000,
255
+ },
256
+ }: RichTextEditorProps) {
257
+ // Pro access kontrolü
258
+ const docsProAccess = { hasAccess: true }; // Pro access assumed in package
259
+ const { hasProAccess, isLoading } = useSubscription();
260
+
261
+ // In docs mode, always show the component
262
+ const canShowComponent = docsProAccess.isDocsMode || hasProAccess;
263
+
264
+ // If not in docs mode and no pro access, show upgrade prompt
265
+ if (!docsProAccess.isDocsMode && !isLoading && !hasProAccess) {
266
+ return (
267
+ <Card className={cn("w-full", className)}>
268
+ <CardContent className="py-12 text-center">
269
+ <div className="max-w-md mx-auto space-y-4">
270
+ <div className="rounded-full bg-purple-100 dark:bg-purple-900/30 p-3 w-fit mx-auto">
271
+ <Lock className="h-6 w-6 text-purple-600 dark:text-purple-400" />
272
+ </div>
273
+ <div>
274
+ <h3 className="font-semibold text-lg mb-2">Pro Feature</h3>
275
+ <p className="text-muted-foreground text-sm mb-4">
276
+ Advanced Rich Text Editor is available exclusively to MoonUI Pro subscribers.
277
+ </p>
278
+ <div className="flex gap-3 justify-center">
279
+ <a href="/pricing">
280
+ <Button size="sm">
281
+ <Sparkles className="mr-2 h-4 w-4" />
282
+ Upgrade to Pro
283
+ </Button>
284
+ </a>
285
+ </div>
286
+ </div>
287
+ </div>
288
+ </CardContent>
289
+ </Card>
290
+ );
291
+ }
292
+
293
+ const [aiSettings, setAiSettings] = useState<AISettingsType>({
294
+ provider: aiConfig.provider || 'openai',
295
+ apiKey: aiConfig.apiKey || '',
296
+ model: aiConfig.model || 'gpt-4',
297
+ temperature: aiConfig.temperature || 0.7,
298
+ maxTokens: aiConfig.maxTokens || 1000,
299
+ });
300
+ const [isAiSettingsOpen, setIsAiSettingsOpen] = useState(false);
301
+ const [isProcessing, setIsProcessing] = useState(false);
302
+ const [isSourceView, setIsSourceView] = useState(false);
303
+ const [sourceContent, setSourceContent] = useState('');
304
+ const [currentTextColor, setCurrentTextColor] = useState('#000000');
305
+ const [currentBgColor, setCurrentBgColor] = useState('#ffffff');
306
+
307
+ // Slash commands tanımları
308
+ const slashCommands: SlashCommand[] = [
309
+ {
310
+ id: 'rewrite',
311
+ command: 'rewrite',
312
+ description: 'Rewrite selected text',
313
+ icon: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.3"/></svg>',
314
+ action: async (text: string) => {
315
+ const result = await callAI('rewrite', text);
316
+ return { text: result || '', error: result ? undefined : 'Failed to rewrite' };
317
+ }
318
+ },
319
+ {
320
+ id: 'expand',
321
+ command: 'expand',
322
+ description: 'Expand text with more details',
323
+ icon: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m21 21-6-6m6 6v-4.8m0 4.8h-4.8"/><path d="M3 16.2V21m0 0h4.8M3 21l6-6"/><path d="M21 7.8V3m0 0h-4.8M21 3l-6 6"/><path d="M3 7.8V3m0 0h4.8M3 3l6 6"/></svg>',
324
+ action: async (text: string) => {
325
+ const result = await callAI('expand', text);
326
+ return { text: result || '', error: result ? undefined : 'Failed to expand' };
327
+ }
328
+ },
329
+ {
330
+ id: 'summarize',
331
+ command: 'summarize',
332
+ description: 'Summarize text',
333
+ icon: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11H3"/><path d="M9 7H3"/><path d="M9 15H3"/><path d="M16 10V3"/><path d="M16 10l3-3"/><path d="M13 10l3 3"/><path d="M9 19h12"/></svg>',
334
+ action: async (text: string) => {
335
+ const result = await callAI('summarize', text);
336
+ return { text: result || '', error: result ? undefined : 'Failed to summarize' };
337
+ }
338
+ },
339
+ {
340
+ id: 'continue',
341
+ command: 'continue',
342
+ description: 'Continue writing from here',
343
+ icon: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>',
344
+ action: async (text: string) => {
345
+ const result = await callAI('continue', text);
346
+ return { text: result || '', error: result ? undefined : 'Failed to continue' };
347
+ }
348
+ },
349
+ {
350
+ id: 'improve',
351
+ command: 'improve',
352
+ description: 'Improve writing quality',
353
+ icon: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/></svg>',
354
+ action: async (text: string) => {
355
+ const result = await callAI('improve', text);
356
+ return { text: result || '', error: result ? undefined : 'Failed to improve' };
357
+ }
358
+ },
359
+ {
360
+ id: 'fix',
361
+ command: 'fix',
362
+ description: 'Fix grammar and spelling',
363
+ icon: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"/></svg>',
364
+ action: async (text: string) => {
365
+ const result = await callAI('fix', text);
366
+ return { text: result || '', error: result ? undefined : 'Failed to fix' };
367
+ }
368
+ },
369
+ {
370
+ id: 'translate',
371
+ command: 'translate',
372
+ description: 'Translate to Turkish',
373
+ icon: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m5 8 6 6"/><path d="m4 14 6-6 2-3"/><path d="M2 5h12"/><path d="M7 2v3"/><path d="m22 22-5-10-5 10"/><path d="m14 18 6 0"/></svg>',
374
+ action: async (text: string) => {
375
+ const result = await callAI('translate', text);
376
+ return { text: result || '', error: result ? undefined : 'Failed to translate' };
377
+ }
378
+ },
379
+ {
380
+ id: 'ideas',
381
+ command: 'ideas',
382
+ description: 'Generate writing ideas',
383
+ icon: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v20"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>',
384
+ action: async (text: string) => {
385
+ const result = await callAI('ideas', text);
386
+ return { text: result || '', error: result ? undefined : 'Failed to generate ideas' };
387
+ }
388
+ }
389
+ ];
390
+
391
+ const editor = useEditor({
392
+ extensions: [
393
+ StarterKit.configure({
394
+ heading: {
395
+ levels: [1, 2, 3],
396
+ },
397
+ codeBlock: false, // We'll use CodeBlockLowlight instead
398
+ }),
399
+ Placeholder.configure({
400
+ placeholder,
401
+ }),
402
+ TextAlign.configure({
403
+ types: ['heading', 'paragraph'],
404
+ }),
405
+ Highlight.configure({
406
+ multicolor: true,
407
+ }),
408
+ Link.configure({
409
+ openOnClick: false,
410
+ HTMLAttributes: {
411
+ target: '_blank',
412
+ rel: 'noopener noreferrer',
413
+ },
414
+ }),
415
+ Image.configure({
416
+ inline: true,
417
+ allowBase64: true,
418
+ }),
419
+ Table.configure({
420
+ resizable: true,
421
+ }),
422
+ TableRow,
423
+ TableHeader,
424
+ TableCell,
425
+ TextStyle,
426
+ Color,
427
+ UnderlineExtension,
428
+ Gapcursor,
429
+ TaskList,
430
+ TaskItem.configure({
431
+ nested: true,
432
+ }),
433
+ Typography,
434
+ CodeBlockLowlight.configure({
435
+ lowlight,
436
+ }),
437
+ SlashCommandsExtension.configure({
438
+ commands: slashCommands,
439
+ onSelectCommand: async (command) => {
440
+ if (!editor) return;
441
+
442
+ const { from, to } = editor.state.selection;
443
+ const selectedText = editor.state.doc.textBetween(from, to, ' ');
444
+
445
+ setIsProcessing(true);
446
+ try {
447
+ const response = await command.action(selectedText || editor.getText());
448
+ if (response.text) {
449
+ editor.chain().focus().insertContent(response.text).run();
450
+ } else if (response.error) {
451
+ toast({
452
+ title: "AI Error",
453
+ description: response.error,
454
+ variant: "destructive",
455
+ });
456
+ }
457
+ } catch {
458
+ toast({
459
+ title: "Error",
460
+ description: "Failed to execute command",
461
+ variant: "destructive",
462
+ });
463
+ } finally {
464
+ setIsProcessing(false);
465
+ }
466
+ },
467
+ }),
468
+ ],
469
+ content: '',
470
+ editorProps: {
471
+ attributes: {
472
+ class: cn(
473
+ 'prose prose-sm sm:prose lg:prose-lg xl:prose-xl focus:outline-none min-h-[200px] p-4 max-w-none',
474
+ // Ensure headings show different sizes
475
+ '[&_h1]:text-3xl [&_h1]:font-bold [&_h1]:mb-4',
476
+ '[&_h2]:text-2xl [&_h2]:font-bold [&_h2]:mb-3',
477
+ '[&_h3]:text-xl [&_h3]:font-bold [&_h3]:mb-2',
478
+ '[&_h4]:text-lg [&_h4]:font-bold [&_h4]:mb-2',
479
+ '[&_h5]:text-base [&_h5]:font-bold [&_h5]:mb-1',
480
+ '[&_h6]:text-sm [&_h6]:font-bold [&_h6]:mb-1',
481
+ // Ensure lists show proper styling
482
+ '[&_ul]:list-disc [&_ul]:pl-6 [&_ul]:mb-4',
483
+ '[&_ol]:list-decimal [&_ol]:pl-6 [&_ol]:mb-4',
484
+ '[&_li]:mb-1',
485
+ // Table styles
486
+ '[&_table]:border-collapse-collapse [&_table]:w-full [&_table]:mb-4',
487
+ '[&_table_td]:border [&_table_td]:border-gray-300 [&_table_td]:p-2',
488
+ '[&_table_th]:border [&_table_th]:border-gray-300 [&_table_th]:p-2 [&_table_th]:bg-gray-50 [&_table_th]:font-semibold',
489
+ // Other styles
490
+ '[&_blockquote]:border-l-4 [&_blockquote]:border-gray-300 [&_blockquote]:pl-4 [&_blockquote]:italic',
491
+ '[&_pre]:bg-gray-100 [&_pre]:dark:bg-gray-800 [&_pre]:p-4 [&_pre]:rounded [&_pre]:overflow-x-auto'
492
+ ),
493
+ },
494
+ },
495
+ immediatelyRender: false,
496
+ });
497
+
498
+ // AI işlevleri
499
+ const callAI = async (action: string, text: string) => {
500
+ if (!aiSettings.apiKey) {
501
+ toast({
502
+ title: "API Key Required",
503
+ description: "Please configure your AI settings first.",
504
+ variant: "destructive",
505
+ });
506
+ setIsAiSettingsOpen(true);
507
+ return null;
508
+ }
509
+
510
+ setIsProcessing(true);
511
+ try {
512
+ const provider = getAIProvider(aiSettings);
513
+ let response;
514
+
515
+ switch (action) {
516
+ case 'rewrite':
517
+ response = await provider.rewrite(text);
518
+ break;
519
+ case 'expand':
520
+ response = await provider.expand(text);
521
+ break;
522
+ case 'summarize':
523
+ response = await provider.summarize(text);
524
+ break;
525
+ case 'fix':
526
+ response = await provider.fixGrammar(text);
527
+ break;
528
+ case 'translate':
529
+ // TODO: Kullanıcıdan hedef dil seçmesini iste
530
+ response = await provider.translate(text, 'Turkish');
531
+ break;
532
+ case 'tone_professional':
533
+ response = await provider.changeTone(text, 'professional');
534
+ break;
535
+ case 'tone_casual':
536
+ response = await provider.changeTone(text, 'casual');
537
+ break;
538
+ case 'tone_friendly':
539
+ response = await provider.changeTone(text, 'friendly');
540
+ break;
541
+ case 'tone_formal':
542
+ response = await provider.changeTone(text, 'formal');
543
+ break;
544
+ case 'continue':
545
+ response = await provider.continueWriting(text);
546
+ break;
547
+ case 'improve':
548
+ response = await provider.improveWriting(text);
549
+ break;
550
+ case 'ideas':
551
+ response = await provider.generateIdeas(text, 5);
552
+ break;
553
+ default:
554
+ response = await provider.complete(action, text);
555
+ }
556
+
557
+ if (response.error) {
558
+ toast({
559
+ title: "AI Error",
560
+ description: response.error,
561
+ variant: "destructive",
562
+ });
563
+ return null;
564
+ }
565
+
566
+ return response.text;
567
+ } catch {
568
+ toast({
569
+ title: "AI Error",
570
+ description: "Failed to process with AI. Please check your settings.",
571
+ variant: "destructive",
572
+ });
573
+ return null;
574
+ } finally {
575
+ setIsProcessing(false);
576
+ }
577
+ };
578
+
579
+ const handleAIAction = async (action: string) => {
580
+ if (!editor) return;
581
+
582
+ const selection = editor.state.selection;
583
+ const selectedText = editor.state.doc.textBetween(selection.from, selection.to, ' ');
584
+
585
+ if (!selectedText && action !== 'complete') {
586
+ toast({
587
+ title: "No text selected",
588
+ description: "Please select some text first.",
589
+ variant: "destructive",
590
+ });
591
+ return;
592
+ }
593
+
594
+ const result = await callAI(action, selectedText || editor.getText());
595
+
596
+ if (result) {
597
+ if (selectedText) {
598
+ editor.chain().focus().deleteSelection().insertContent(result).run();
599
+ } else {
600
+ editor.chain().focus().insertContent(result).run();
601
+ }
602
+ }
603
+ };
604
+
605
+ const [linkUrl, setLinkUrl] = useState('');
606
+ const [imageUrl, setImageUrl] = useState('');
607
+ const [isLinkDialogOpen, setIsLinkDialogOpen] = useState(false);
608
+ const [isImageDialogOpen, setIsImageDialogOpen] = useState(false);
609
+ const [isTableDialogOpen, setIsTableDialogOpen] = useState(false);
610
+ const [tableRows, setTableRows] = useState(3);
611
+ const [tableCols, setTableCols] = useState(3);
612
+ const [isTableBorderDialogOpen, setIsTableBorderDialogOpen] = useState(false);
613
+ const [tableBorderWidth, setTableBorderWidth] = useState(1);
614
+ const [tableBorderColor, setTableBorderColor] = useState('#000000');
615
+ const [tableBorderStyle, setTableBorderStyle] = useState('solid');
616
+
617
+ const addImage = () => {
618
+ if (imageUrl && editor) {
619
+ editor.chain().focus().setImage({ src: imageUrl }).run();
620
+ setImageUrl('');
621
+ setIsImageDialogOpen(false);
622
+ }
623
+ };
624
+
625
+ const addLink = () => {
626
+ if (linkUrl && editor) {
627
+ if (editor.state.selection.empty) {
628
+ // Eğer seçim yoksa, link'i text olarak ekle
629
+ editor.chain().focus().insertContent(`<a href="${linkUrl}">${linkUrl}</a>`).run();
630
+ } else {
631
+ // Seçili metne link ekle
632
+ editor.chain().focus().extendMarkRange('link').setLink({ href: linkUrl }).run();
633
+ }
634
+ setLinkUrl('');
635
+ setIsLinkDialogOpen(false);
636
+ }
637
+ };
638
+
639
+ const removeLink = () => {
640
+ editor?.chain().focus().unsetLink().run();
641
+ };
642
+
643
+ const createTable = () => {
644
+ if (editor && tableRows > 0 && tableCols > 0) {
645
+ editor.chain().focus().insertTable({
646
+ rows: tableRows,
647
+ cols: tableCols,
648
+ withHeaderRow: true
649
+ }).run();
650
+ // Apply default border styles to the newly created table
651
+ setTimeout(() => {
652
+ applyTableBorders(tableBorderWidth, tableBorderColor, tableBorderStyle);
653
+ }, 100);
654
+ setIsTableDialogOpen(false);
655
+ }
656
+ };
657
+
658
+ const applyTableBorders = (width: number, color: string, style: string) => {
659
+ if (!editor) return;
660
+
661
+ const borderStyle = `${width}px ${style} ${color}`;
662
+
663
+ // Apply borders to the selected table
664
+ editor.view.dom.querySelectorAll('.ProseMirror table').forEach((table) => {
665
+ if (table instanceof HTMLElement) {
666
+ table.style.borderCollapse = 'collapse';
667
+ table.style.border = borderStyle;
668
+
669
+ // Apply borders to all cells
670
+ table.querySelectorAll('td, th').forEach((cell) => {
671
+ if (cell instanceof HTMLElement) {
672
+ cell.style.border = borderStyle;
673
+ cell.style.padding = '8px';
674
+ }
675
+ });
676
+ }
677
+ });
678
+ };
679
+
680
+ if (!editor) {
681
+ return null;
682
+ }
683
+
684
+ return (
685
+ <div className={cn("border rounded-lg overflow-hidden bg-white dark:bg-gray-950", className)}>
686
+ {/* Toolbar */}
687
+ <TooltipProvider>
688
+ <div className="border-b bg-gray-50 dark:bg-gray-900 p-3">
689
+ <div className="flex items-center flex-wrap gap-1">
690
+ {/* Format Group */}
691
+ {(features.bold || features.italic || features.underline || features.strike || features.code) && (
692
+ <div className="flex items-center gap-1 shrink-0">
693
+ {features.bold && (
694
+ <ToolbarButton
695
+ active={editor.isActive('bold')}
696
+ onClick={() => editor.chain().focus().toggleBold().run()}
697
+ tooltip="Bold (Cmd+B)"
698
+ disabled={isSourceView}
699
+ >
700
+ <Bold className="w-4 h-4" />
701
+ </ToolbarButton>
702
+ )}
703
+ {features.italic && (
704
+ <ToolbarButton
705
+ active={editor.isActive('italic')}
706
+ onClick={() => editor.chain().focus().toggleItalic().run()}
707
+ tooltip="Italic (Cmd+I)"
708
+ disabled={isSourceView}
709
+ >
710
+ <Italic className="w-4 h-4" />
711
+ </ToolbarButton>
712
+ )}
713
+ {features.underline && (
714
+ <ToolbarButton
715
+ active={editor.isActive('underline')}
716
+ onClick={() => editor.chain().focus().toggleUnderline().run()}
717
+ tooltip="Underline (Cmd+U)"
718
+ disabled={isSourceView}
719
+ >
720
+ <Underline className="w-4 h-4" />
721
+ </ToolbarButton>
722
+ )}
723
+ {features.strike && (
724
+ <ToolbarButton
725
+ active={editor.isActive('strike')}
726
+ onClick={() => editor.chain().focus().toggleStrike().run()}
727
+ tooltip="Strikethrough"
728
+ disabled={isSourceView}
729
+ >
730
+ <Strikethrough className="w-4 h-4" />
731
+ </ToolbarButton>
732
+ )}
733
+ {features.code && (
734
+ <ToolbarButton
735
+ active={editor.isActive('code')}
736
+ onClick={() => editor.chain().focus().toggleCode().run()}
737
+ tooltip="Inline code"
738
+ disabled={isSourceView}
739
+ >
740
+ <Code className="w-4 h-4" />
741
+ </ToolbarButton>
742
+ )}
743
+ </div>
744
+ )}
745
+
746
+ {/* Separator */}
747
+ {(features.bold || features.italic || features.underline || features.strike || features.code) &&
748
+ (features.heading || features.align) && (
749
+ <div className="w-px h-6 bg-border mx-1 shrink-0"></div>
750
+ )}
751
+
752
+ {/* Typography Group */}
753
+ {(features.heading || features.align) && (
754
+ <div className="flex items-center gap-1 shrink-0">
755
+ {features.heading && (
756
+ <DropdownMenu>
757
+ <DropdownMenuTrigger asChild>
758
+ <Button variant="ghost" size="sm" className="h-8 px-2" disabled={isSourceView}>
759
+ <Type className="w-4 h-4 mr-1" />
760
+ <ChevronDown className="w-3 h-3" />
761
+ </Button>
762
+ </DropdownMenuTrigger>
763
+ <DropdownMenuContent>
764
+ <DropdownMenuItem onClick={() => editor.chain().focus().setParagraph().run()}>
765
+ Paragraph
766
+ </DropdownMenuItem>
767
+ <DropdownMenuItem onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}>
768
+ <Heading1 className="w-4 h-4 mr-2" /> Heading 1
769
+ </DropdownMenuItem>
770
+ <DropdownMenuItem onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}>
771
+ <Heading2 className="w-4 h-4 mr-2" /> Heading 2
772
+ </DropdownMenuItem>
773
+ <DropdownMenuItem onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}>
774
+ <Heading3 className="w-4 h-4 mr-2" /> Heading 3
775
+ </DropdownMenuItem>
776
+ </DropdownMenuContent>
777
+ </DropdownMenu>
778
+ )}
779
+
780
+ {features.align && (
781
+ <>
782
+ <ToolbarButton
783
+ active={editor.isActive({ textAlign: 'left' })}
784
+ onClick={() => editor.chain().focus().setTextAlign('left').run()}
785
+ tooltip="Align left"
786
+ disabled={isSourceView}
787
+ >
788
+ <AlignLeft className="w-4 h-4" />
789
+ </ToolbarButton>
790
+ <ToolbarButton
791
+ active={editor.isActive({ textAlign: 'center' })}
792
+ onClick={() => editor.chain().focus().setTextAlign('center').run()}
793
+ tooltip="Align center"
794
+ disabled={isSourceView}
795
+ >
796
+ <AlignCenter className="w-4 h-4" />
797
+ </ToolbarButton>
798
+ <ToolbarButton
799
+ active={editor.isActive({ textAlign: 'right' })}
800
+ onClick={() => editor.chain().focus().setTextAlign('right').run()}
801
+ tooltip="Align right"
802
+ disabled={isSourceView}
803
+ >
804
+ <AlignRight className="w-4 h-4" />
805
+ </ToolbarButton>
806
+ <ToolbarButton
807
+ active={editor.isActive({ textAlign: 'justify' })}
808
+ onClick={() => editor.chain().focus().setTextAlign('justify').run()}
809
+ tooltip="Justify"
810
+ disabled={isSourceView}
811
+ >
812
+ <AlignJustify className="w-4 h-4" />
813
+ </ToolbarButton>
814
+ </>
815
+ )}
816
+ </div>
817
+ )}
818
+
819
+ {/* Separator */}
820
+ {(features.heading || features.align) &&
821
+ (features.lists || features.blockquote) && (
822
+ <div className="w-px h-6 bg-border mx-1 shrink-0"></div>
823
+ )}
824
+
825
+ {/* Lists & Structure Group */}
826
+ {(features.lists || features.blockquote) && (
827
+ <div className="flex items-center gap-1 shrink-0">
828
+ {features.lists && (
829
+ <>
830
+ <ToolbarButton
831
+ active={editor.isActive('bulletList')}
832
+ onClick={() => editor.chain().focus().toggleBulletList().run()}
833
+ tooltip="Bullet list"
834
+ disabled={isSourceView}
835
+ >
836
+ <List className="w-4 h-4" />
837
+ </ToolbarButton>
838
+ <ToolbarButton
839
+ active={editor.isActive('orderedList')}
840
+ onClick={() => editor.chain().focus().toggleOrderedList().run()}
841
+ tooltip="Numbered list"
842
+ disabled={isSourceView}
843
+ >
844
+ <ListOrdered className="w-4 h-4" />
845
+ </ToolbarButton>
846
+ <ToolbarButton
847
+ active={editor.isActive('taskList')}
848
+ onClick={() => editor.chain().focus().toggleTaskList().run()}
849
+ tooltip="Task list"
850
+ disabled={isSourceView}
851
+ >
852
+ <CheckSquare className="w-4 h-4" />
853
+ </ToolbarButton>
854
+ </>
855
+ )}
856
+
857
+ {features.blockquote && (
858
+ <ToolbarButton
859
+ active={editor.isActive('blockquote')}
860
+ onClick={() => editor.chain().focus().toggleBlockquote().run()}
861
+ tooltip="Quote"
862
+ disabled={isSourceView}
863
+ >
864
+ <Quote className="w-4 h-4" />
865
+ </ToolbarButton>
866
+ )}
867
+ </div>
868
+ )}
869
+
870
+ {/* Separator */}
871
+ {(features.lists || features.blockquote) &&
872
+ (features.color) && (
873
+ <div className="w-px h-6 bg-border mx-1 shrink-0"></div>
874
+ )}
875
+
876
+ {/* Colors Group */}
877
+ {features.color && (
878
+ <div className="flex items-center gap-1 shrink-0">
879
+ <Popover>
880
+ <PopoverTrigger asChild>
881
+ <Button
882
+ variant="ghost"
883
+ size="sm"
884
+ className="h-8 px-2"
885
+ disabled={isSourceView}
886
+ title="Text Color"
887
+ >
888
+ <Palette className="w-4 h-4" />
889
+ </Button>
890
+ </PopoverTrigger>
891
+ <PopoverContent className="w-auto p-0">
892
+ <EditorColorPicker
893
+ currentColor={currentTextColor}
894
+ onColorSelect={(color) => {
895
+ setCurrentTextColor(color);
896
+ editor.chain().focus().setColor(color).run();
897
+ }}
898
+ />
899
+ </PopoverContent>
900
+ </Popover>
901
+
902
+ <Popover>
903
+ <PopoverTrigger asChild>
904
+ <Button
905
+ variant="ghost"
906
+ size="sm"
907
+ className="h-8 px-2"
908
+ disabled={isSourceView}
909
+ title="Background Color"
910
+ >
911
+ <Highlighter className="w-4 h-4" />
912
+ </Button>
913
+ </PopoverTrigger>
914
+ <PopoverContent className="w-auto p-0">
915
+ <EditorColorPicker
916
+ currentColor={currentBgColor}
917
+ onColorSelect={(color) => {
918
+ setCurrentBgColor(color);
919
+ editor.chain().focus().toggleHighlight({ color }).run();
920
+ }}
921
+ />
922
+ </PopoverContent>
923
+ </Popover>
924
+ </div>
925
+ )}
926
+
927
+ {/* Separator */}
928
+ {(features.color) &&
929
+ (features.link || features.image || features.table) && (
930
+ <div className="w-px h-6 bg-border mx-1 shrink-0"></div>
931
+ )}
932
+
933
+ {/* Media & Links Group */}
934
+ {(features.link || features.image || features.table) && (
935
+ <div className="flex items-center gap-1 shrink-0">
936
+ {features.link && (
937
+ <Dialog open={isLinkDialogOpen} onOpenChange={setIsLinkDialogOpen}>
938
+ <DialogTrigger asChild>
939
+ <ToolbarButton
940
+ active={editor.isActive('link')}
941
+ tooltip="Add link"
942
+ disabled={isSourceView}
943
+ >
944
+ <Link2 className="w-4 h-4" />
945
+ </ToolbarButton>
946
+ </DialogTrigger>
947
+ <DialogContent className="sm:max-w-[425px]">
948
+ <DialogHeader>
949
+ <DialogTitle>Add Link</DialogTitle>
950
+ <DialogDescription>
951
+ Enter the URL for the link.
952
+ </DialogDescription>
953
+ </DialogHeader>
954
+ <div className="grid gap-4 py-4">
955
+ <div className="grid gap-2">
956
+ <Label htmlFor="url">URL</Label>
957
+ <Input
958
+ id="url"
959
+ value={linkUrl}
960
+ onChange={(e) => setLinkUrl(e.target.value)}
961
+ placeholder="https://example.com"
962
+ onKeyDown={(e) => {
963
+ if (e.key === 'Enter') {
964
+ e.preventDefault();
965
+ addLink();
966
+ }
967
+ }}
968
+ />
969
+ </div>
970
+ </div>
971
+ <div className="flex justify-between">
972
+ {editor.isActive('link') && (
973
+ <Button
974
+ variant="destructive"
975
+ onClick={() => {
976
+ removeLink();
977
+ setIsLinkDialogOpen(false);
978
+ }}
979
+ >
980
+ Remove Link
981
+ </Button>
982
+ )}
983
+ <div className="ml-auto flex gap-2">
984
+ <Button variant="outline" onClick={() => setIsLinkDialogOpen(false)}>
985
+ Cancel
986
+ </Button>
987
+ <Button onClick={addLink}>Add Link</Button>
988
+ </div>
989
+ </div>
990
+ </DialogContent>
991
+ </Dialog>
992
+ )}
993
+ {features.image && (
994
+ <Dialog open={isImageDialogOpen} onOpenChange={setIsImageDialogOpen}>
995
+ <DialogTrigger asChild>
996
+ <ToolbarButton
997
+ tooltip="Add image"
998
+ disabled={isSourceView}
999
+ >
1000
+ <ImageIcon className="w-4 h-4" />
1001
+ </ToolbarButton>
1002
+ </DialogTrigger>
1003
+ <DialogContent className="sm:max-w-[425px]">
1004
+ <DialogHeader>
1005
+ <DialogTitle>Add Image</DialogTitle>
1006
+ <DialogDescription>
1007
+ Enter the URL for the image.
1008
+ </DialogDescription>
1009
+ </DialogHeader>
1010
+ <div className="grid gap-4 py-4">
1011
+ <div className="grid gap-2">
1012
+ <Label htmlFor="image-url">Image URL</Label>
1013
+ <Input
1014
+ id="image-url"
1015
+ value={imageUrl}
1016
+ onChange={(e) => setImageUrl(e.target.value)}
1017
+ placeholder="https://example.com/image.jpg"
1018
+ onKeyDown={(e) => {
1019
+ if (e.key === 'Enter') {
1020
+ e.preventDefault();
1021
+ addImage();
1022
+ }
1023
+ }}
1024
+ />
1025
+ </div>
1026
+ </div>
1027
+ <div className="flex justify-end gap-2">
1028
+ <Button variant="outline" onClick={() => setIsImageDialogOpen(false)}>
1029
+ Cancel
1030
+ </Button>
1031
+ <Button onClick={addImage}>Add Image</Button>
1032
+ </div>
1033
+ </DialogContent>
1034
+ </Dialog>
1035
+ )}
1036
+ {features.table && (
1037
+ <>
1038
+ <Dialog open={isTableDialogOpen} onOpenChange={setIsTableDialogOpen}>
1039
+ <DialogTrigger asChild>
1040
+ <ToolbarButton
1041
+ tooltip="Create table"
1042
+ disabled={isSourceView}
1043
+ >
1044
+ <TableIcon className="w-4 h-4" />
1045
+ </ToolbarButton>
1046
+ </DialogTrigger>
1047
+ <DialogContent className="sm:max-w-[425px]">
1048
+ <DialogHeader>
1049
+ <DialogTitle>Create Table</DialogTitle>
1050
+ <DialogDescription>
1051
+ Choose the size for your new table.
1052
+ </DialogDescription>
1053
+ </DialogHeader>
1054
+ <div className="grid gap-4 py-4">
1055
+ <div className="grid grid-cols-2 gap-4">
1056
+ <div className="grid gap-2">
1057
+ <Label htmlFor="rows">Rows</Label>
1058
+ <Input
1059
+ id="rows"
1060
+ type="number"
1061
+ min="1"
1062
+ max="20"
1063
+ value={tableRows}
1064
+ onChange={(e) => setTableRows(parseInt(e.target.value) || 3)}
1065
+ />
1066
+ </div>
1067
+ <div className="grid gap-2">
1068
+ <Label htmlFor="cols">Columns</Label>
1069
+ <Input
1070
+ id="cols"
1071
+ type="number"
1072
+ min="1"
1073
+ max="10"
1074
+ value={tableCols}
1075
+ onChange={(e) => setTableCols(parseInt(e.target.value) || 3)}
1076
+ />
1077
+ </div>
1078
+ </div>
1079
+ <div className="flex items-center space-x-2">
1080
+ <input
1081
+ type="checkbox"
1082
+ id="headerRow"
1083
+ defaultChecked
1084
+ className="rounded border-gray-300"
1085
+ />
1086
+ <Label htmlFor="headerRow" className="text-sm font-normal">
1087
+ Include header row
1088
+ </Label>
1089
+ </div>
1090
+ </div>
1091
+ <div className="flex justify-end gap-2">
1092
+ <Button variant="outline" onClick={() => setIsTableDialogOpen(false)}>
1093
+ Cancel
1094
+ </Button>
1095
+ <Button onClick={createTable}>Create Table</Button>
1096
+ </div>
1097
+ </DialogContent>
1098
+ </Dialog>
1099
+ <DropdownMenu>
1100
+ <DropdownMenuTrigger asChild>
1101
+ <Button variant="ghost" size="sm" className="h-8 px-1">
1102
+ <ChevronDown className="w-3 h-3" />
1103
+ </Button>
1104
+ </DropdownMenuTrigger>
1105
+ <DropdownMenuContent>
1106
+ <DropdownMenuItem onClick={() => editor.chain().focus().addColumnBefore().run()}>
1107
+ Add column before
1108
+ </DropdownMenuItem>
1109
+ <DropdownMenuItem onClick={() => editor.chain().focus().addColumnAfter().run()}>
1110
+ Add column after
1111
+ </DropdownMenuItem>
1112
+ <DropdownMenuItem onClick={() => editor.chain().focus().deleteColumn().run()}>
1113
+ Delete column
1114
+ </DropdownMenuItem>
1115
+ <DropdownMenuSeparator />
1116
+ <DropdownMenuItem onClick={() => editor.chain().focus().addRowBefore().run()}>
1117
+ Add row before
1118
+ </DropdownMenuItem>
1119
+ <DropdownMenuItem onClick={() => editor.chain().focus().addRowAfter().run()}>
1120
+ Add row after
1121
+ </DropdownMenuItem>
1122
+ <DropdownMenuItem onClick={() => editor.chain().focus().deleteRow().run()}>
1123
+ Delete row
1124
+ </DropdownMenuItem>
1125
+ <DropdownMenuSeparator />
1126
+ <DropdownMenuItem onClick={() => setIsTableBorderDialogOpen(true)}>
1127
+ <Settings className="w-4 h-4 mr-2" />
1128
+ Table Borders
1129
+ </DropdownMenuItem>
1130
+ <DropdownMenuSeparator />
1131
+ <DropdownMenuItem onClick={() => editor.chain().focus().deleteTable().run()}>
1132
+ Delete table
1133
+ </DropdownMenuItem>
1134
+ </DropdownMenuContent>
1135
+ </DropdownMenu>
1136
+ </>
1137
+ )}
1138
+ </div>
1139
+ )}
1140
+
1141
+ {/* Separator */}
1142
+ {(features.link || features.image || features.table) &&
1143
+ (true) && (
1144
+ <div className="w-px h-6 bg-border mx-1 shrink-0"></div>
1145
+ )}
1146
+
1147
+ {/* Tools Group */}
1148
+ <div className="flex items-center gap-1 shrink-0">
1149
+ <ToolbarButton
1150
+ onClick={() => editor.chain().focus().undo().run()}
1151
+ disabled={!editor.can().undo() || isSourceView}
1152
+ tooltip="Undo (Cmd+Z)"
1153
+ >
1154
+ <Undo className="w-4 h-4" />
1155
+ </ToolbarButton>
1156
+ <ToolbarButton
1157
+ onClick={() => editor.chain().focus().redo().run()}
1158
+ disabled={!editor.can().redo() || isSourceView}
1159
+ tooltip="Redo (Cmd+Shift+Z)"
1160
+ >
1161
+ <Redo className="w-4 h-4" />
1162
+ </ToolbarButton>
1163
+
1164
+ <ToolbarButton
1165
+ active={isSourceView}
1166
+ onClick={() => {
1167
+ if (isSourceView) {
1168
+ // Apply source changes when switching back to visual mode
1169
+ editor.commands.setContent(sourceContent);
1170
+ } else {
1171
+ // Update source content when switching to source view
1172
+ setSourceContent(editor.getHTML());
1173
+ }
1174
+ setIsSourceView(!isSourceView);
1175
+ }}
1176
+ tooltip={isSourceView ? "Visual Mode" : "Source Code"}
1177
+ >
1178
+ {isSourceView ? <Edit className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
1179
+ </ToolbarButton>
1180
+
1181
+ {/* AI Tools */}
1182
+ {features.ai && (
1183
+ <>
1184
+ <DropdownMenu>
1185
+ <DropdownMenuTrigger asChild>
1186
+ <Button
1187
+ variant="ghost"
1188
+ size="sm"
1189
+ className="h-8 px-3 bg-purple-100 hover:bg-purple-200 dark:bg-purple-900 dark:hover:bg-purple-800"
1190
+ disabled={isProcessing}
1191
+ >
1192
+ {isProcessing ? (
1193
+ <RefreshCw className="w-4 h-4 mr-1 animate-spin" />
1194
+ ) : (
1195
+ <Wand2 className="w-4 h-4 mr-1" />
1196
+ )}
1197
+ AI Tools
1198
+ </Button>
1199
+ </DropdownMenuTrigger>
1200
+ <DropdownMenuContent className="w-56">
1201
+ <DropdownMenuItem onClick={() => handleAIAction('rewrite')}>
1202
+ <RefreshCw className="w-4 h-4 mr-2" />
1203
+ Rewrite Selection
1204
+ </DropdownMenuItem>
1205
+ <DropdownMenuItem onClick={() => handleAIAction('improve')}>
1206
+ <Wand2 className="w-4 h-4 mr-2" />
1207
+ Improve Writing
1208
+ </DropdownMenuItem>
1209
+ <DropdownMenuItem onClick={() => handleAIAction('expand')}>
1210
+ <Maximize className="w-4 h-4 mr-2" />
1211
+ Expand Text
1212
+ </DropdownMenuItem>
1213
+ <DropdownMenuItem onClick={() => handleAIAction('summarize')}>
1214
+ <FileText className="w-4 h-4 mr-2" />
1215
+ Summarize
1216
+ </DropdownMenuItem>
1217
+ <DropdownMenuItem onClick={() => handleAIAction('continue')}>
1218
+ <Plus className="w-4 h-4 mr-2" />
1219
+ Continue Writing
1220
+ </DropdownMenuItem>
1221
+ <DropdownMenuSeparator />
1222
+ <DropdownMenuItem onClick={() => handleAIAction('fix')}>
1223
+ <Check className="w-4 h-4 mr-2" />
1224
+ Fix Grammar & Spelling
1225
+ </DropdownMenuItem>
1226
+ <DropdownMenuSeparator />
1227
+ <DropdownMenuItem onClick={() => handleAIAction('tone_professional')}>
1228
+ <Sparkles className="w-4 h-4 mr-2" />
1229
+ Make Professional
1230
+ </DropdownMenuItem>
1231
+ <DropdownMenuItem onClick={() => handleAIAction('tone_casual')}>
1232
+ <Sparkles className="w-4 h-4 mr-2" />
1233
+ Make Casual
1234
+ </DropdownMenuItem>
1235
+ <DropdownMenuItem onClick={() => handleAIAction('tone_friendly')}>
1236
+ <Sparkles className="w-4 h-4 mr-2" />
1237
+ Make Friendly
1238
+ </DropdownMenuItem>
1239
+ <DropdownMenuItem onClick={() => handleAIAction('tone_formal')}>
1240
+ <Sparkles className="w-4 h-4 mr-2" />
1241
+ Make Formal
1242
+ </DropdownMenuItem>
1243
+ <DropdownMenuSeparator />
1244
+ <DropdownMenuItem onClick={() => handleAIAction('translate')}>
1245
+ <Languages className="w-4 h-4 mr-2" />
1246
+ Translate to Turkish
1247
+ </DropdownMenuItem>
1248
+ <DropdownMenuItem onClick={() => handleAIAction('ideas')}>
1249
+ <Sparkles className="w-4 h-4 mr-2" />
1250
+ Generate Ideas
1251
+ </DropdownMenuItem>
1252
+ </DropdownMenuContent>
1253
+ </DropdownMenu>
1254
+
1255
+ <Dialog open={isAiSettingsOpen} onOpenChange={setIsAiSettingsOpen}>
1256
+ <DialogTrigger asChild>
1257
+ <ToolbarButton
1258
+ tooltip="AI Settings - Configure your API keys here"
1259
+ disabled={isSourceView}
1260
+ >
1261
+ <Settings className="w-4 h-4" />
1262
+ </ToolbarButton>
1263
+ </DialogTrigger>
1264
+ <DialogContent className="sm:max-w-[425px]">
1265
+ <DialogHeader>
1266
+ <DialogTitle>AI Settings</DialogTitle>
1267
+ <DialogDescription>
1268
+ Configure your AI provider and API settings.
1269
+ </DialogDescription>
1270
+ </DialogHeader>
1271
+ <div className="grid gap-4 py-4">
1272
+ <div className="grid gap-2">
1273
+ <Label htmlFor="provider">Provider</Label>
1274
+ <Select
1275
+ value={aiSettings.provider}
1276
+ onValueChange={(value: 'openai' | 'claude' | 'gemini' | 'cohere') => setAiSettings({ ...aiSettings, provider: value })}
1277
+ >
1278
+ <SelectTrigger>
1279
+ <SelectValue />
1280
+ </SelectTrigger>
1281
+ <SelectContent>
1282
+ <SelectItem value="openai">OpenAI</SelectItem>
1283
+ <SelectItem value="claude">Claude (Anthropic)</SelectItem>
1284
+ <SelectItem value="gemini">Gemini (Google)</SelectItem>
1285
+ <SelectItem value="cohere">Cohere</SelectItem>
1286
+ </SelectContent>
1287
+ </Select>
1288
+ </div>
1289
+ <div className="grid gap-2">
1290
+ <Label htmlFor="apiKey">API Key</Label>
1291
+ <Input
1292
+ id="apiKey"
1293
+ type="password"
1294
+ value={aiSettings.apiKey}
1295
+ onChange={(e) => setAiSettings({ ...aiSettings, apiKey: e.target.value })}
1296
+ placeholder="sk-..."
1297
+ />
1298
+ </div>
1299
+ <div className="grid gap-2">
1300
+ <Label htmlFor="model">Model</Label>
1301
+ <Select
1302
+ value={aiSettings.model}
1303
+ onValueChange={(value) => setAiSettings({ ...aiSettings, model: value })}
1304
+ >
1305
+ <SelectTrigger>
1306
+ <SelectValue />
1307
+ </SelectTrigger>
1308
+ <SelectContent>
1309
+ {aiSettings.provider === 'openai' && (
1310
+ <>
1311
+ <SelectItem value="gpt-4">GPT-4</SelectItem>
1312
+ <SelectItem value="gpt-3.5-turbo">GPT-3.5 Turbo</SelectItem>
1313
+ </>
1314
+ )}
1315
+ {aiSettings.provider === 'claude' && (
1316
+ <>
1317
+ <SelectItem value="claude-3-opus">Claude 3 Opus</SelectItem>
1318
+ <SelectItem value="claude-3-sonnet">Claude 3 Sonnet</SelectItem>
1319
+ <SelectItem value="claude-3-haiku">Claude 3 Haiku</SelectItem>
1320
+ </>
1321
+ )}
1322
+ {aiSettings.provider === 'gemini' && (
1323
+ <>
1324
+ <SelectItem value="gemini-pro">Gemini Pro</SelectItem>
1325
+ <SelectItem value="gemini-pro-vision">Gemini Pro Vision</SelectItem>
1326
+ </>
1327
+ )}
1328
+ {aiSettings.provider === 'cohere' && (
1329
+ <>
1330
+ <SelectItem value="command">Command</SelectItem>
1331
+ <SelectItem value="command-light">Command Light</SelectItem>
1332
+ </>
1333
+ )}
1334
+ </SelectContent>
1335
+ </Select>
1336
+ </div>
1337
+ <div className="grid grid-cols-2 gap-4">
1338
+ <div className="grid gap-2">
1339
+ <Label htmlFor="temperature">Temperature</Label>
1340
+ <Input
1341
+ id="temperature"
1342
+ type="number"
1343
+ min="0"
1344
+ max="2"
1345
+ step="0.1"
1346
+ value={aiSettings.temperature}
1347
+ onChange={(e) => setAiSettings({ ...aiSettings, temperature: parseFloat(e.target.value) })}
1348
+ />
1349
+ </div>
1350
+ <div className="grid gap-2">
1351
+ <Label htmlFor="maxTokens">Max Tokens</Label>
1352
+ <Input
1353
+ id="maxTokens"
1354
+ type="number"
1355
+ min="1"
1356
+ max="4000"
1357
+ value={aiSettings.maxTokens}
1358
+ onChange={(e) => setAiSettings({ ...aiSettings, maxTokens: parseInt(e.target.value) })}
1359
+ />
1360
+ </div>
1361
+ </div>
1362
+ </div>
1363
+ <div className="flex justify-end">
1364
+ <Button onClick={() => {
1365
+ setIsAiSettingsOpen(false);
1366
+ toast({
1367
+ title: "Settings saved",
1368
+ description: "Your AI settings have been updated.",
1369
+ });
1370
+ }}>
1371
+ Save Settings
1372
+ </Button>
1373
+ </div>
1374
+ </DialogContent>
1375
+ </Dialog>
1376
+ </>
1377
+ )}
1378
+ </div>
1379
+ </div>
1380
+ </div>
1381
+ </TooltipProvider>
1382
+
1383
+ {/* Table Border Settings Dialog */}
1384
+ <Dialog open={isTableBorderDialogOpen} onOpenChange={setIsTableBorderDialogOpen}>
1385
+ <DialogContent className="sm:max-w-[425px]">
1386
+ <DialogHeader>
1387
+ <DialogTitle>Table Border Settings</DialogTitle>
1388
+ <DialogDescription>
1389
+ Customize the appearance of table borders.
1390
+ </DialogDescription>
1391
+ </DialogHeader>
1392
+ <div className="grid gap-6 py-4">
1393
+ <div className="grid gap-3">
1394
+ <Label htmlFor="border-width">Border Width</Label>
1395
+ <div className="flex items-center gap-4">
1396
+ <Slider
1397
+ id="border-width"
1398
+ min={0}
1399
+ max={5}
1400
+ step={1}
1401
+ value={[tableBorderWidth]}
1402
+ onValueChange={(value) => setTableBorderWidth(value[0])}
1403
+ className="flex-1"
1404
+ />
1405
+ <span className="text-sm font-medium w-8">{tableBorderWidth}px</span>
1406
+ </div>
1407
+ </div>
1408
+
1409
+ <div className="grid gap-3">
1410
+ <Label htmlFor="border-color">Border Color</Label>
1411
+ <div className="flex items-center gap-2">
1412
+ <ColorPicker
1413
+ value={tableBorderColor}
1414
+ onChange={setTableBorderColor}
1415
+ showInput={true}
1416
+ showPresets={false}
1417
+ size="sm"
1418
+ />
1419
+ <span className="text-sm text-muted-foreground">{tableBorderColor}</span>
1420
+ </div>
1421
+ </div>
1422
+
1423
+ <div className="grid gap-3">
1424
+ <Label htmlFor="border-style">Border Style</Label>
1425
+ <Select value={tableBorderStyle} onValueChange={setTableBorderStyle}>
1426
+ <SelectTrigger>
1427
+ <SelectValue />
1428
+ </SelectTrigger>
1429
+ <SelectContent>
1430
+ <SelectItem value="solid">Solid</SelectItem>
1431
+ <SelectItem value="dashed">Dashed</SelectItem>
1432
+ <SelectItem value="dotted">Dotted</SelectItem>
1433
+ <SelectItem value="double">Double</SelectItem>
1434
+ <SelectItem value="groove">Groove</SelectItem>
1435
+ <SelectItem value="ridge">Ridge</SelectItem>
1436
+ <SelectItem value="inset">Inset</SelectItem>
1437
+ <SelectItem value="outset">Outset</SelectItem>
1438
+ </SelectContent>
1439
+ </Select>
1440
+ </div>
1441
+
1442
+ {/* Preview */}
1443
+ <div className="grid gap-3">
1444
+ <Label>Preview</Label>
1445
+ <div className="p-4 bg-gray-50 dark:bg-gray-900 rounded-lg">
1446
+ <table
1447
+ className="w-full text-sm"
1448
+ style={{
1449
+ borderCollapse: 'collapse',
1450
+ border: `${tableBorderWidth}px ${tableBorderStyle} ${tableBorderColor}`
1451
+ }}
1452
+ >
1453
+ <thead>
1454
+ <tr>
1455
+ <th
1456
+ style={{
1457
+ border: `${tableBorderWidth}px ${tableBorderStyle} ${tableBorderColor}`,
1458
+ padding: '8px',
1459
+ textAlign: 'left'
1460
+ }}
1461
+ >
1462
+ Header 1
1463
+ </th>
1464
+ <th
1465
+ style={{
1466
+ border: `${tableBorderWidth}px ${tableBorderStyle} ${tableBorderColor}`,
1467
+ padding: '8px',
1468
+ textAlign: 'left'
1469
+ }}
1470
+ >
1471
+ Header 2
1472
+ </th>
1473
+ </tr>
1474
+ </thead>
1475
+ <tbody>
1476
+ <tr>
1477
+ <td
1478
+ style={{
1479
+ border: `${tableBorderWidth}px ${tableBorderStyle} ${tableBorderColor}`,
1480
+ padding: '8px'
1481
+ }}
1482
+ >
1483
+ Cell 1
1484
+ </td>
1485
+ <td
1486
+ style={{
1487
+ border: `${tableBorderWidth}px ${tableBorderStyle} ${tableBorderColor}`,
1488
+ padding: '8px'
1489
+ }}
1490
+ >
1491
+ Cell 2
1492
+ </td>
1493
+ </tr>
1494
+ </tbody>
1495
+ </table>
1496
+ </div>
1497
+ </div>
1498
+ </div>
1499
+ <div className="flex justify-end gap-2">
1500
+ <Button variant="outline" onClick={() => setIsTableBorderDialogOpen(false)}>
1501
+ Cancel
1502
+ </Button>
1503
+ <Button onClick={() => {
1504
+ applyTableBorders(tableBorderWidth, tableBorderColor, tableBorderStyle);
1505
+ setIsTableBorderDialogOpen(false);
1506
+ toast({
1507
+ title: "Borders applied",
1508
+ description: "Table borders have been updated.",
1509
+ });
1510
+ }}>
1511
+ Apply Borders
1512
+ </Button>
1513
+ </div>
1514
+ </DialogContent>
1515
+ </Dialog>
1516
+
1517
+ {/* Editor */}
1518
+ <div
1519
+ className="overflow-auto"
1520
+ style={{ height: typeof height === 'number' ? `${height}px` : height }}
1521
+ >
1522
+ {isSourceView ? (
1523
+ <textarea
1524
+ value={sourceContent}
1525
+ onChange={(e) => setSourceContent(e.target.value)}
1526
+ className="w-full h-full p-4 font-mono text-sm resize-none focus:outline-none bg-gray-50 dark:bg-gray-900"
1527
+ placeholder="HTML source code..."
1528
+ />
1529
+ ) : (
1530
+ <EditorContent editor={editor} />
1531
+ )}
1532
+ </div>
1533
+ </div>
1534
+ );
1535
+ }
1536
+
1537
+ export default RichTextEditor;