@moontra/moonui 2.0.8 → 2.0.9

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 (94) hide show
  1. package/package.json +9 -2
  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/ui/__tests__/accordion.test.tsx +571 -0
  6. package/src/components/ui/__tests__/alert.test.tsx +484 -0
  7. package/src/components/ui/__tests__/aspect-ratio.test.tsx +492 -0
  8. package/src/components/ui/__tests__/avatar.test.tsx +382 -0
  9. package/src/components/ui/__tests__/badge.test.tsx +364 -0
  10. package/src/components/ui/__tests__/breadcrumb.test.tsx +687 -0
  11. package/src/components/ui/__tests__/button.test.tsx +91 -0
  12. package/src/components/ui/__tests__/card.test.tsx +104 -0
  13. package/src/components/ui/__tests__/checkbox.test.tsx +591 -0
  14. package/src/components/ui/__tests__/collapsible.test.tsx +592 -0
  15. package/src/components/ui/__tests__/color-picker.test.tsx +365 -0
  16. package/src/components/ui/__tests__/command.test.tsx +677 -0
  17. package/src/components/ui/__tests__/data-table.test.tsx +256 -0
  18. package/src/components/ui/__tests__/date-picker.test.tsx +320 -0
  19. package/src/components/ui/__tests__/dialog.test.tsx +764 -0
  20. package/src/components/ui/__tests__/input.test.tsx +318 -0
  21. package/src/components/ui/__tests__/label.test.tsx +103 -0
  22. package/src/components/ui/__tests__/popover.test.tsx +637 -0
  23. package/src/components/ui/__tests__/progress.test.tsx +382 -0
  24. package/src/components/ui/__tests__/radio-group.test.tsx +555 -0
  25. package/src/components/ui/__tests__/select.test.tsx +705 -0
  26. package/src/components/ui/__tests__/skeleton.test.tsx +253 -0
  27. package/src/components/ui/__tests__/slider.test.tsx +493 -0
  28. package/src/components/ui/__tests__/switch.test.tsx +372 -0
  29. package/src/components/ui/__tests__/table.test.tsx +824 -0
  30. package/src/components/ui/__tests__/tabs.test.tsx +887 -0
  31. package/src/components/ui/__tests__/toast.test.tsx +458 -0
  32. package/src/components/ui/__tests__/tooltip.test.tsx +583 -0
  33. package/src/components/ui/accordion.tsx +63 -0
  34. package/src/components/ui/alert.tsx +130 -0
  35. package/src/components/ui/aspect-ratio.tsx +72 -0
  36. package/src/components/ui/avatar.tsx +135 -0
  37. package/src/components/ui/badge.tsx +225 -0
  38. package/src/components/ui/breadcrumb.tsx +207 -0
  39. package/src/components/ui/button.stories.tsx +162 -0
  40. package/src/components/ui/button.tsx +221 -0
  41. package/src/components/ui/calendar.tsx +262 -0
  42. package/src/components/ui/card.stories.tsx +208 -0
  43. package/src/components/ui/card.tsx +141 -0
  44. package/src/components/ui/checkbox.tsx +256 -0
  45. package/src/components/ui/collapsible.tsx +129 -0
  46. package/src/components/ui/color-picker.tsx +664 -0
  47. package/src/components/ui/command.tsx +218 -0
  48. package/src/components/ui/data-table.stories.tsx +509 -0
  49. package/src/components/ui/data-table.tsx +623 -0
  50. package/src/components/ui/date-picker.stories.tsx +321 -0
  51. package/src/components/ui/date-picker.tsx +603 -0
  52. package/src/components/ui/dialog.tsx +332 -0
  53. package/src/components/ui/dropdown-menu.tsx +200 -0
  54. package/src/components/ui/file-upload.tsx +400 -0
  55. package/src/components/ui/github-stars.tsx +201 -0
  56. package/src/components/ui/index.ts +12 -0
  57. package/src/components/ui/input.stories.tsx +255 -0
  58. package/src/components/ui/input.tsx +219 -0
  59. package/src/components/ui/label.tsx +26 -0
  60. package/src/components/ui/locked-component.tsx +215 -0
  61. package/src/components/ui/moon-logo.tsx +97 -0
  62. package/src/components/ui/pagination.tsx +116 -0
  63. package/src/components/ui/popover.tsx +183 -0
  64. package/src/components/ui/progress.tsx +182 -0
  65. package/src/components/ui/radio-group.tsx +243 -0
  66. package/src/components/ui/select.tsx +273 -0
  67. package/src/components/ui/separator.tsx +140 -0
  68. package/src/components/ui/simple-editor.tsx +652 -0
  69. package/src/components/ui/skeleton.tsx +351 -0
  70. package/src/components/ui/slider.tsx +351 -0
  71. package/src/components/ui/switch.tsx +83 -0
  72. package/src/components/ui/table.tsx +322 -0
  73. package/src/components/ui/tabs.tsx +195 -0
  74. package/src/components/ui/textarea.tsx +46 -0
  75. package/src/components/ui/toast.tsx +313 -0
  76. package/src/components/ui/toggle.tsx +47 -0
  77. package/src/components/ui/tooltip.tsx +152 -0
  78. package/src/docs/theme-system.md +1194 -0
  79. package/src/index.tsx +39 -0
  80. package/src/lib/micro-interactions.ts +255 -0
  81. package/src/lib/theme.tsx +398 -0
  82. package/src/lib/utils.ts +69 -0
  83. package/src/styles/design-system.css +365 -0
  84. package/src/styles/index.css +4 -0
  85. package/src/styles/tailwind.css +6 -0
  86. package/src/styles/tokens.css +453 -0
  87. package/src/use-intersection-observer.tsx +154 -0
  88. package/src/use-local-storage.tsx +71 -0
  89. package/src/use-paddle.ts +138 -0
  90. package/src/use-performance-optimizer.ts +379 -0
  91. package/src/use-pro-access.ts +141 -0
  92. package/src/use-scroll-animation.ts +221 -0
  93. package/src/use-subscription.ts +37 -0
  94. package/src/use-toast.ts +32 -0
@@ -0,0 +1,652 @@
1
+ "use client"
2
+
3
+ import React, { useCallback, useState, useEffect } from 'react'
4
+ import { motion } from 'framer-motion'
5
+ import { Button } from './button'
6
+ import { Separator } from './separator'
7
+ import { Toggle } from './toggle'
8
+ import { Popover, PopoverContent, PopoverTrigger } from './popover'
9
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './select'
10
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './tooltip'
11
+ import { cn } from '../../lib/utils'
12
+ import {
13
+ Bold,
14
+ Italic,
15
+ Underline,
16
+ Strikethrough,
17
+ AlignLeft,
18
+ AlignCenter,
19
+ AlignRight,
20
+ AlignJustify,
21
+ List,
22
+ ListOrdered,
23
+ Quote,
24
+ Link,
25
+ Image as ImageIcon,
26
+ Code,
27
+ Type,
28
+ Palette,
29
+ Undo,
30
+ Redo,
31
+ Eye,
32
+ Edit
33
+ } from 'lucide-react'
34
+
35
+ export interface SimpleEditorProps {
36
+ value?: string
37
+ onChange?: (value: string) => void
38
+ placeholder?: string
39
+ disabled?: boolean
40
+ className?: string
41
+ minHeight?: number
42
+ maxHeight?: number
43
+ showToolbar?: boolean
44
+ showPreview?: boolean
45
+ }
46
+
47
+ interface ToolbarButtonProps {
48
+ icon: React.ReactNode
49
+ tooltip: string
50
+ active?: boolean
51
+ onClick: () => void
52
+ disabled?: boolean
53
+ }
54
+
55
+ const ToolbarButton = ({ icon, tooltip, active, onClick, disabled }: ToolbarButtonProps) => (
56
+ <Tooltip>
57
+ <TooltipTrigger asChild>
58
+ <Toggle
59
+ pressed={active}
60
+ onPressedChange={() => onClick()}
61
+ disabled={disabled}
62
+ className="h-8 w-8 p-0"
63
+ >
64
+ {icon}
65
+ </Toggle>
66
+ </TooltipTrigger>
67
+ <TooltipContent>
68
+ <p>{tooltip}</p>
69
+ </TooltipContent>
70
+ </Tooltip>
71
+ )
72
+
73
+ const ColorPicker = ({ onColorSelect }: { onColorSelect: (color: string) => void }) => {
74
+ const colors = [
75
+ '#000000', '#374151', '#6B7280', '#9CA3AF',
76
+ '#EF4444', '#F59E0B', '#EAB308', '#22C55E',
77
+ '#3B82F6', '#6366F1', '#8B5CF6', '#EC4899'
78
+ ]
79
+
80
+ return (
81
+ <div className="grid grid-cols-4 gap-1 p-2">
82
+ {colors.map(color => (
83
+ <button
84
+ key={color}
85
+ className="w-6 h-6 rounded border-2 border-gray-200 hover:border-gray-400"
86
+ style={{ backgroundColor: color }}
87
+ onClick={() => onColorSelect(color)}
88
+ />
89
+ ))}
90
+ </div>
91
+ )
92
+ }
93
+
94
+ export const SimpleEditor = React.forwardRef<HTMLDivElement, SimpleEditorProps>(
95
+ ({
96
+ value = '',
97
+ onChange,
98
+ placeholder = 'Start writing...',
99
+ disabled = false,
100
+ className,
101
+ minHeight = 200,
102
+ maxHeight = 500,
103
+ showToolbar = true,
104
+ showPreview = true,
105
+ ...props
106
+ }, ref) => {
107
+ const [content, setContent] = useState(value)
108
+ const [isPreview, setIsPreview] = useState(false)
109
+ const [sourceContent, setSourceContent] = useState(value)
110
+ const [selection, setSelection] = useState<{ start: number; end: number } | null>(null)
111
+ const editorRef = React.useRef<HTMLDivElement>(null)
112
+ const sourceRef = React.useRef<HTMLTextAreaElement>(null)
113
+
114
+ // Format state tracking
115
+ const [formatState, setFormatState] = useState({
116
+ bold: false,
117
+ italic: false,
118
+ underline: false,
119
+ strikethrough: false,
120
+ alignLeft: true,
121
+ alignCenter: false,
122
+ alignRight: false,
123
+ alignJustify: false,
124
+ orderedList: false,
125
+ unorderedList: false,
126
+ quote: false,
127
+ code: false
128
+ })
129
+
130
+ const updateContent = useCallback((newContent: string) => {
131
+ setContent(newContent)
132
+ onChange?.(newContent)
133
+ }, [onChange])
134
+
135
+ const saveSelection = useCallback(() => {
136
+ const selection = window.getSelection()
137
+ if (selection && selection.rangeCount > 0) {
138
+ return selection.getRangeAt(0)
139
+ }
140
+ return null
141
+ }, [])
142
+
143
+ const restoreSelection = useCallback((range: Range | null) => {
144
+ if (!range) return
145
+
146
+ const selection = window.getSelection()
147
+ if (selection) {
148
+ selection.removeAllRanges()
149
+ selection.addRange(range)
150
+ }
151
+ }, [])
152
+
153
+ const executeCommand = useCallback((command: string, value?: string) => {
154
+ if (disabled) return
155
+
156
+ // Focus the editor first
157
+ if (editorRef.current) {
158
+ editorRef.current.focus()
159
+ }
160
+
161
+ // Save current selection before executing command
162
+ const savedRange = saveSelection()
163
+
164
+ // Execute the command
165
+ const result = document.execCommand(command, false, value)
166
+
167
+ if (!result) {
168
+ console.warn(`Command '${command}' failed to execute`)
169
+ }
170
+
171
+ // Update content state to trigger re-render
172
+ if (editorRef.current) {
173
+ const newContent = editorRef.current.innerHTML
174
+ updateContent(newContent)
175
+ }
176
+
177
+ // Update format state
178
+ setFormatState({
179
+ bold: document.queryCommandState('bold'),
180
+ italic: document.queryCommandState('italic'),
181
+ underline: document.queryCommandState('underline'),
182
+ strikethrough: document.queryCommandState('strikeThrough'),
183
+ alignLeft: document.queryCommandState('justifyLeft'),
184
+ alignCenter: document.queryCommandState('justifyCenter'),
185
+ alignRight: document.queryCommandState('justifyRight'),
186
+ alignJustify: document.queryCommandState('justifyFull'),
187
+ orderedList: document.queryCommandState('insertOrderedList'),
188
+ unorderedList: document.queryCommandState('insertUnorderedList'),
189
+ quote: document.queryCommandState('formatBlock'),
190
+ code: document.queryCommandState('formatBlock')
191
+ })
192
+
193
+ // Keep focus on editor
194
+ if (editorRef.current) {
195
+ editorRef.current.focus()
196
+ }
197
+ }, [disabled, saveSelection, updateContent])
198
+
199
+ const handleInput = useCallback((e: React.FormEvent<HTMLDivElement>) => {
200
+ const newContent = e.currentTarget.innerHTML
201
+ updateContent(newContent)
202
+ }, [updateContent])
203
+
204
+ const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
205
+ // Handle keyboard shortcuts
206
+ if (e.ctrlKey || e.metaKey) {
207
+ switch (e.key) {
208
+ case 'b':
209
+ e.preventDefault()
210
+ executeCommand('bold')
211
+ break
212
+ case 'i':
213
+ e.preventDefault()
214
+ executeCommand('italic')
215
+ break
216
+ case 'u':
217
+ e.preventDefault()
218
+ executeCommand('underline')
219
+ break
220
+ case 'z':
221
+ e.preventDefault()
222
+ if (e.shiftKey) {
223
+ executeCommand('redo')
224
+ } else {
225
+ executeCommand('undo')
226
+ }
227
+ break
228
+ }
229
+ }
230
+
231
+ // Handle Tab for indentation
232
+ if (e.key === 'Tab') {
233
+ e.preventDefault()
234
+ executeCommand('insertHTML', '&nbsp;&nbsp;&nbsp;&nbsp;')
235
+ }
236
+ }, [executeCommand])
237
+
238
+ const insertLink = useCallback(() => {
239
+ const url = prompt('Enter URL:')
240
+ if (url) {
241
+ executeCommand('createLink', url)
242
+ }
243
+ }, [executeCommand])
244
+
245
+ const insertImage = useCallback(() => {
246
+ const url = prompt('Enter image URL:')
247
+ if (url) {
248
+ executeCommand('insertImage', url)
249
+ }
250
+ }, [executeCommand])
251
+
252
+ const formatHeading = useCallback((level: string) => {
253
+ executeCommand('formatBlock', `<h${level}>`)
254
+ }, [executeCommand])
255
+
256
+ const setTextColor = useCallback((color: string) => {
257
+ // First try the modern way
258
+ const selection = window.getSelection()
259
+ if (selection && selection.rangeCount > 0) {
260
+ const range = selection.getRangeAt(0)
261
+ const span = document.createElement('span')
262
+ span.style.color = color
263
+
264
+ try {
265
+ range.surroundContents(span)
266
+ } catch (e) {
267
+ // If surroundContents fails, use execCommand
268
+ executeCommand('foreColor', color)
269
+ }
270
+
271
+ // Update content
272
+ if (editorRef.current) {
273
+ const newContent = editorRef.current.innerHTML
274
+ updateContent(newContent)
275
+ }
276
+ }
277
+ }, [executeCommand, updateContent])
278
+
279
+ // Convert HTML to plain text for preview
280
+ const getPlainText = (html: string) => {
281
+ if (typeof window === 'undefined') {
282
+ // Server-side: simple regex-based HTML stripping
283
+ return html
284
+ .replace(/<[^>]*>/g, '') // Remove HTML tags
285
+ .replace(/&nbsp;/g, ' ') // Replace &nbsp; with space
286
+ .replace(/&amp;/g, '&') // Replace &amp; with &
287
+ .replace(/&lt;/g, '<') // Replace &lt; with <
288
+ .replace(/&gt;/g, '>') // Replace &gt; with >
289
+ .replace(/&quot;/g, '"') // Replace &quot; with "
290
+ .replace(/&#39;/g, "'") // Replace &#39; with '
291
+ }
292
+ // Client-side: use DOM for accurate conversion
293
+ const div = document.createElement('div')
294
+ div.innerHTML = html
295
+ return div.textContent || div.innerText || ''
296
+ }
297
+
298
+ React.useEffect(() => {
299
+ if (editorRef.current && content !== editorRef.current.innerHTML) {
300
+ editorRef.current.innerHTML = content
301
+ }
302
+ }, [content])
303
+
304
+ // Sync source content when value prop changes
305
+ React.useEffect(() => {
306
+ if (value !== undefined && value !== content) {
307
+ setContent(value)
308
+ setSourceContent(value)
309
+ }
310
+ }, [value, content])
311
+
312
+ const toolbar = showToolbar && (
313
+ <TooltipProvider>
314
+ <motion.div
315
+ className="flex items-center gap-1 p-2 border-b bg-muted/50"
316
+ initial={{ opacity: 0, y: -10 }}
317
+ animate={{ opacity: 1, y: 0 }}
318
+ >
319
+ {/* Text Formatting */}
320
+ {!isPreview && (
321
+ <>
322
+ <div className="flex items-center gap-1">
323
+ <ToolbarButton
324
+ icon={<Bold className="h-3 w-3" />}
325
+ tooltip="Bold (Ctrl+B)"
326
+ active={formatState.bold}
327
+ onClick={() => executeCommand('bold')}
328
+ disabled={disabled}
329
+ />
330
+ <ToolbarButton
331
+ icon={<Italic className="h-3 w-3" />}
332
+ tooltip="Italic (Ctrl+I)"
333
+ active={formatState.italic}
334
+ onClick={() => executeCommand('italic')}
335
+ disabled={disabled}
336
+ />
337
+ <ToolbarButton
338
+ icon={<Underline className="h-3 w-3" />}
339
+ tooltip="Underline (Ctrl+U)"
340
+ active={formatState.underline}
341
+ onClick={() => executeCommand('underline')}
342
+ disabled={disabled}
343
+ />
344
+ <ToolbarButton
345
+ icon={<Strikethrough className="h-3 w-3" />}
346
+ tooltip="Strikethrough"
347
+ active={formatState.strikethrough}
348
+ onClick={() => executeCommand('strikeThrough')}
349
+ disabled={disabled}
350
+ />
351
+ </div>
352
+
353
+ <Separator orientation="vertical" className="h-6" />
354
+
355
+ {/* Font Size / Heading */}
356
+ <Select
357
+ onValueChange={(value) => {
358
+ if (value.startsWith('h')) {
359
+ formatHeading(value.substring(1))
360
+ } else if (value === 'normal') {
361
+ executeCommand('formatBlock', 'p')
362
+ } else {
363
+ executeCommand('fontSize', value)
364
+ }
365
+ }}
366
+ disabled={disabled}
367
+ >
368
+ <SelectTrigger className="w-24 h-8">
369
+ <SelectValue placeholder="Size" />
370
+ </SelectTrigger>
371
+ <SelectContent>
372
+ <SelectItem value="1">Small</SelectItem>
373
+ <SelectItem value="3">Normal</SelectItem>
374
+ <SelectItem value="5">Large</SelectItem>
375
+ <SelectItem value="7">X-Large</SelectItem>
376
+ <Separator className="my-1" />
377
+ <SelectItem value="h1">Heading 1</SelectItem>
378
+ <SelectItem value="h2">Heading 2</SelectItem>
379
+ <SelectItem value="h3">Heading 3</SelectItem>
380
+ <SelectItem value="h4">Heading 4</SelectItem>
381
+ <SelectItem value="h5">Heading 5</SelectItem>
382
+ <SelectItem value="h6">Heading 6</SelectItem>
383
+ </SelectContent>
384
+ </Select>
385
+
386
+ <Separator orientation="vertical" className="h-6" />
387
+
388
+ {/* Alignment */}
389
+ <div className="flex items-center gap-1">
390
+ <ToolbarButton
391
+ icon={<AlignLeft className="h-3 w-3" />}
392
+ tooltip="Align Left"
393
+ active={formatState.alignLeft}
394
+ onClick={() => executeCommand('justifyLeft')}
395
+ disabled={disabled}
396
+ />
397
+ <ToolbarButton
398
+ icon={<AlignCenter className="h-3 w-3" />}
399
+ tooltip="Align Center"
400
+ active={formatState.alignCenter}
401
+ onClick={() => executeCommand('justifyCenter')}
402
+ disabled={disabled}
403
+ />
404
+ <ToolbarButton
405
+ icon={<AlignRight className="h-3 w-3" />}
406
+ tooltip="Align Right"
407
+ active={formatState.alignRight}
408
+ onClick={() => executeCommand('justifyRight')}
409
+ disabled={disabled}
410
+ />
411
+ <ToolbarButton
412
+ icon={<AlignJustify className="h-3 w-3" />}
413
+ tooltip="Justify"
414
+ active={formatState.alignJustify}
415
+ onClick={() => executeCommand('justifyFull')}
416
+ disabled={disabled}
417
+ />
418
+ </div>
419
+
420
+ <Separator orientation="vertical" className="h-6" />
421
+
422
+ {/* Lists */}
423
+ <div className="flex items-center gap-1">
424
+ <ToolbarButton
425
+ icon={<List className="h-3 w-3" />}
426
+ tooltip="Bullet List"
427
+ active={formatState.unorderedList}
428
+ onClick={() => executeCommand('insertUnorderedList')}
429
+ disabled={disabled}
430
+ />
431
+ <ToolbarButton
432
+ icon={<ListOrdered className="h-3 w-3" />}
433
+ tooltip="Numbered List"
434
+ active={formatState.orderedList}
435
+ onClick={() => executeCommand('insertOrderedList')}
436
+ disabled={disabled}
437
+ />
438
+ <ToolbarButton
439
+ icon={<Quote className="h-3 w-3" />}
440
+ tooltip="Quote"
441
+ active={formatState.quote}
442
+ onClick={() => executeCommand('formatBlock', 'blockquote')}
443
+ disabled={disabled}
444
+ />
445
+ <ToolbarButton
446
+ icon={<Code className="h-3 w-3" />}
447
+ tooltip="Code Block"
448
+ active={formatState.code}
449
+ onClick={() => executeCommand('formatBlock', 'pre')}
450
+ disabled={disabled}
451
+ />
452
+ </div>
453
+
454
+ <Separator orientation="vertical" className="h-6" />
455
+
456
+ {/* Indentation */}
457
+ <div className="flex items-center gap-1">
458
+ <ToolbarButton
459
+ icon={<span className="text-xs font-mono">←</span>}
460
+ tooltip="Decrease Indent"
461
+ onClick={() => executeCommand('outdent')}
462
+ disabled={disabled}
463
+ />
464
+ <ToolbarButton
465
+ icon={<span className="text-xs font-mono">→</span>}
466
+ tooltip="Increase Indent"
467
+ onClick={() => executeCommand('indent')}
468
+ disabled={disabled}
469
+ />
470
+ </div>
471
+
472
+ <Separator orientation="vertical" className="h-6" />
473
+
474
+ {/* Insert */}
475
+ <div className="flex items-center gap-1">
476
+ <ToolbarButton
477
+ icon={<Link className="h-3 w-3" />}
478
+ tooltip="Insert Link"
479
+ onClick={insertLink}
480
+ disabled={disabled}
481
+ />
482
+ <ToolbarButton
483
+ icon={<ImageIcon className="h-3 w-3" />}
484
+ tooltip="Insert Image"
485
+ onClick={insertImage}
486
+ disabled={disabled}
487
+ />
488
+ </div>
489
+
490
+ <Separator orientation="vertical" className="h-6" />
491
+
492
+ {/* Color */}
493
+ <Popover>
494
+ <PopoverTrigger asChild>
495
+ <Button
496
+ variant="ghost"
497
+ size="sm"
498
+ className="h-8 w-8 p-0"
499
+ disabled={disabled}
500
+ title="Text Color"
501
+ >
502
+ <Palette className="h-3 w-3" />
503
+ </Button>
504
+ </PopoverTrigger>
505
+ <PopoverContent className="w-auto p-0">
506
+ <ColorPicker onColorSelect={setTextColor} />
507
+ </PopoverContent>
508
+ </Popover>
509
+
510
+ <Separator orientation="vertical" className="h-6" />
511
+
512
+ {/* Undo/Redo */}
513
+ <div className="flex items-center gap-1">
514
+ <ToolbarButton
515
+ icon={<Undo className="h-3 w-3" />}
516
+ tooltip="Undo (Ctrl+Z)"
517
+ onClick={() => executeCommand('undo')}
518
+ disabled={disabled}
519
+ />
520
+ <ToolbarButton
521
+ icon={<Redo className="h-3 w-3" />}
522
+ tooltip="Redo (Ctrl+Shift+Z)"
523
+ onClick={() => executeCommand('redo')}
524
+ disabled={disabled}
525
+ />
526
+ </div>
527
+
528
+ </>
529
+ )}
530
+
531
+ {/* Preview Toggle */}
532
+ {showPreview && (
533
+ <>
534
+ {!isPreview && <Separator orientation="vertical" className="h-6" />}
535
+ <ToolbarButton
536
+ icon={isPreview ? <Edit className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
537
+ tooltip={isPreview ? "Edit Mode" : "View Source Code"}
538
+ active={isPreview}
539
+ onClick={() => {
540
+ if (isPreview) {
541
+ // Apply source changes to content when switching back to edit mode
542
+ updateContent(sourceContent)
543
+ } else {
544
+ // Update source content when switching to preview mode
545
+ setSourceContent(content)
546
+ }
547
+ setIsPreview(!isPreview)
548
+ }}
549
+ disabled={disabled}
550
+ />
551
+ </>
552
+ )}
553
+ </motion.div>
554
+ </TooltipProvider>
555
+ )
556
+
557
+ return (
558
+ <div
559
+ ref={ref}
560
+ className={cn(
561
+ "border rounded-lg overflow-hidden focus-within:ring-2 focus-within:ring-ring",
562
+ disabled && "opacity-50 cursor-not-allowed",
563
+ className
564
+ )}
565
+ {...props}
566
+ >
567
+ {toolbar}
568
+
569
+ <div className="relative">
570
+ {isPreview ? (
571
+ <motion.div
572
+ initial={{ opacity: 0 }}
573
+ animate={{ opacity: 1 }}
574
+ className="p-4"
575
+ style={{ minHeight, maxHeight, overflow: 'auto' }}
576
+ >
577
+ <textarea
578
+ ref={sourceRef}
579
+ value={sourceContent}
580
+ onChange={(e) => setSourceContent(e.target.value)}
581
+ className="w-full h-full bg-muted rounded-lg p-4 font-mono text-sm resize-none focus:outline-none focus:ring-2 focus:ring-ring"
582
+ style={{ minHeight: minHeight - 32 }}
583
+ disabled={disabled}
584
+ placeholder="HTML source code..."
585
+ />
586
+ </motion.div>
587
+ ) : (
588
+ <motion.div
589
+ ref={editorRef}
590
+ contentEditable={!disabled}
591
+ suppressContentEditableWarning
592
+ className={cn(
593
+ "p-4 outline-none prose prose-sm dark:prose-invert max-w-none overflow-y-auto",
594
+ "focus:ring-0",
595
+ "[&_h1]:text-3xl [&_h1]:font-bold [&_h1]:mb-4",
596
+ "[&_h2]:text-2xl [&_h2]:font-bold [&_h2]:mb-3",
597
+ "[&_h3]:text-xl [&_h3]:font-bold [&_h3]:mb-2",
598
+ "[&_h4]:text-lg [&_h4]:font-bold [&_h4]:mb-2",
599
+ "[&_h5]:text-base [&_h5]:font-bold [&_h5]:mb-1",
600
+ "[&_h6]:text-sm [&_h6]:font-bold [&_h6]:mb-1",
601
+ "[&_ul]:list-disc [&_ul]:pl-6 [&_ul]:mb-4",
602
+ "[&_ol]:list-decimal [&_ol]:pl-6 [&_ol]:mb-4",
603
+ "[&_li]:mb-1",
604
+ "[&_blockquote]:border-l-4 [&_blockquote]:border-gray-300 [&_blockquote]:pl-4 [&_blockquote]:italic",
605
+ "[&_pre]:bg-gray-100 [&_pre]:dark:bg-gray-800 [&_pre]:p-4 [&_pre]:rounded [&_pre]:overflow-x-auto"
606
+ )}
607
+ style={{ minHeight, maxHeight }}
608
+ onInput={handleInput}
609
+ onKeyDown={handleKeyDown}
610
+ data-placeholder={placeholder}
611
+ initial={{ opacity: 0 }}
612
+ animate={{ opacity: 1 }}
613
+ />
614
+ )}
615
+
616
+ {/* Placeholder */}
617
+ {!content && !isPreview && (
618
+ <div
619
+ className="absolute top-4 left-4 text-muted-foreground pointer-events-none"
620
+ style={{ fontSize: '16px' }}
621
+ >
622
+ {placeholder}
623
+ </div>
624
+ )}
625
+ </div>
626
+
627
+ {/* Character Count */}
628
+ <div className="px-4 py-2 border-t bg-muted/50 text-xs text-muted-foreground">
629
+ <CharacterCount content={content} />
630
+ </div>
631
+ </div>
632
+ )
633
+ }
634
+ )
635
+
636
+ SimpleEditor.displayName = "SimpleEditor"
637
+
638
+ // Character count component to avoid hydration issues
639
+ const CharacterCount = ({ content }: { content: string }) => {
640
+ const [count, setCount] = useState(0)
641
+
642
+ useEffect(() => {
643
+ const getPlainText = (html: string) => {
644
+ const div = document.createElement('div')
645
+ div.innerHTML = html
646
+ return div.textContent || div.innerText || ''
647
+ }
648
+ setCount(getPlainText(content).length)
649
+ }, [content])
650
+
651
+ return <>{count} characters</>
652
+ }