@moontra/moonui-pro 2.0.22 → 2.1.0

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 (99) hide show
  1. package/dist/index.mjs +215 -214
  2. package/package.json +4 -2
  3. package/src/__tests__/use-intersection-observer.test.tsx +216 -0
  4. package/src/__tests__/use-local-storage.test.tsx +174 -0
  5. package/src/__tests__/use-pro-access.test.tsx +183 -0
  6. package/src/components/advanced-chart/advanced-chart.test.tsx +281 -0
  7. package/src/components/advanced-chart/index.tsx +412 -0
  8. package/src/components/advanced-forms/index.tsx +431 -0
  9. package/src/components/animated-button/index.tsx +202 -0
  10. package/src/components/calendar/event-dialog.tsx +372 -0
  11. package/src/components/calendar/index.tsx +557 -0
  12. package/src/components/color-picker/index.tsx +434 -0
  13. package/src/components/dashboard/index.tsx +334 -0
  14. package/src/components/data-table/data-table.test.tsx +187 -0
  15. package/src/components/data-table/index.tsx +368 -0
  16. package/src/components/draggable-list/index.tsx +100 -0
  17. package/src/components/enhanced/button.tsx +360 -0
  18. package/src/components/enhanced/card.tsx +272 -0
  19. package/src/components/enhanced/dialog.tsx +248 -0
  20. package/src/components/enhanced/index.ts +3 -0
  21. package/src/components/error-boundary/index.tsx +111 -0
  22. package/src/components/file-upload/file-upload.test.tsx +242 -0
  23. package/src/components/file-upload/index.tsx +362 -0
  24. package/src/components/floating-action-button/index.tsx +209 -0
  25. package/src/components/github-stars/index.tsx +414 -0
  26. package/src/components/health-check/index.tsx +441 -0
  27. package/src/components/hover-card-3d/index.tsx +170 -0
  28. package/src/components/index.ts +76 -0
  29. package/src/components/kanban/index.tsx +436 -0
  30. package/src/components/lazy-component/index.tsx +342 -0
  31. package/src/components/magnetic-button/index.tsx +170 -0
  32. package/src/components/memory-efficient-data/index.tsx +352 -0
  33. package/src/components/optimized-image/index.tsx +427 -0
  34. package/src/components/performance-debugger/index.tsx +591 -0
  35. package/src/components/performance-monitor/index.tsx +775 -0
  36. package/src/components/pinch-zoom/index.tsx +172 -0
  37. package/src/components/rich-text-editor/index-old-backup.tsx +443 -0
  38. package/src/components/rich-text-editor/index.tsx +1537 -0
  39. package/src/components/rich-text-editor/slash-commands-extension.ts +220 -0
  40. package/src/components/rich-text-editor/slash-commands.css +35 -0
  41. package/src/components/rich-text-editor/table-styles.css +65 -0
  42. package/src/components/spotlight-card/index.tsx +194 -0
  43. package/src/components/swipeable-card/index.tsx +100 -0
  44. package/src/components/timeline/index.tsx +333 -0
  45. package/src/components/ui/animated-button.tsx +185 -0
  46. package/src/components/ui/avatar.tsx +135 -0
  47. package/src/components/ui/badge.tsx +225 -0
  48. package/src/components/ui/button.tsx +221 -0
  49. package/src/components/ui/card.tsx +141 -0
  50. package/src/components/ui/checkbox.tsx +256 -0
  51. package/src/components/ui/color-picker.tsx +95 -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/hover-card-3d.tsx +103 -0
  55. package/src/components/ui/index.ts +33 -0
  56. package/src/components/ui/input.tsx +219 -0
  57. package/src/components/ui/label.tsx +26 -0
  58. package/src/components/ui/magnetic-button.tsx +129 -0
  59. package/src/components/ui/popover.tsx +183 -0
  60. package/src/components/ui/select.tsx +273 -0
  61. package/src/components/ui/separator.tsx +140 -0
  62. package/src/components/ui/slider.tsx +351 -0
  63. package/src/components/ui/spotlight-card.tsx +119 -0
  64. package/src/components/ui/switch.tsx +83 -0
  65. package/src/components/ui/tabs.tsx +195 -0
  66. package/src/components/ui/textarea.tsx +25 -0
  67. package/src/components/ui/toast.tsx +313 -0
  68. package/src/components/ui/tooltip.tsx +152 -0
  69. package/src/components/virtual-list/index.tsx +369 -0
  70. package/src/hooks/use-chart.ts +205 -0
  71. package/src/hooks/use-data-table.ts +182 -0
  72. package/src/hooks/use-docs-pro-access.ts +13 -0
  73. package/src/hooks/use-license-check.ts +65 -0
  74. package/src/hooks/use-subscription.ts +19 -0
  75. package/src/index.ts +14 -0
  76. package/src/lib/micro-interactions.ts +255 -0
  77. package/src/lib/utils.ts +6 -0
  78. package/src/patterns/login-form/index.tsx +276 -0
  79. package/src/patterns/login-form/types.ts +67 -0
  80. package/src/setupTests.ts +41 -0
  81. package/src/styles/design-system.css +365 -0
  82. package/src/styles/index.css +4 -0
  83. package/src/styles/tailwind.css +6 -0
  84. package/src/styles/tokens.css +453 -0
  85. package/src/types/moonui.d.ts +22 -0
  86. package/src/use-intersection-observer.tsx +154 -0
  87. package/src/use-local-storage.tsx +71 -0
  88. package/src/use-paddle.ts +138 -0
  89. package/src/use-performance-optimizer.ts +379 -0
  90. package/src/use-pro-access.ts +141 -0
  91. package/src/use-scroll-animation.ts +221 -0
  92. package/src/use-subscription.ts +37 -0
  93. package/src/use-toast.ts +32 -0
  94. package/src/utils/chart-helpers.ts +257 -0
  95. package/src/utils/cn.ts +69 -0
  96. package/src/utils/data-processing.ts +151 -0
  97. package/src/utils/license-guard.tsx +177 -0
  98. package/src/utils/license-validator.tsx +183 -0
  99. package/src/utils/package-guard.ts +60 -0
@@ -0,0 +1,172 @@
1
+ "use client"
2
+
3
+ import React, { useRef, useState, useCallback } from "react"
4
+ import { motion, useMotionValue, useTransform, animate } from "framer-motion"
5
+ import { cn } from "../../lib/utils"
6
+ import { Card, CardContent } from "../ui/card"
7
+ import { Button } from "../ui/button"
8
+ import { Lock, Sparkles } from "lucide-react"
9
+ import { useSubscription } from "../../hooks/use-subscription"
10
+
11
+ export interface PinchZoomProps {
12
+ children: React.ReactNode
13
+ minZoom?: number
14
+ maxZoom?: number
15
+ initialZoom?: number
16
+ className?: string
17
+ contentClassName?: string
18
+ onZoomChange?: (zoom: number) => void
19
+ }
20
+
21
+ const PinchZoomInternal = React.forwardRef<HTMLDivElement, PinchZoomProps>(
22
+ ({
23
+ children,
24
+ minZoom = 0.5,
25
+ maxZoom = 3,
26
+ initialZoom = 1,
27
+ className,
28
+ contentClassName,
29
+ onZoomChange,
30
+ ...props
31
+ }, ref) => {
32
+ const containerRef = useRef<HTMLDivElement>(null)
33
+ const [isDragging, setIsDragging] = useState(false)
34
+
35
+ const scale = useMotionValue(initialZoom)
36
+ const x = useMotionValue(0)
37
+ const y = useMotionValue(0)
38
+
39
+ const constrainedScale = useTransform(scale, (value) => {
40
+ return Math.min(Math.max(value, minZoom), maxZoom)
41
+ })
42
+
43
+ const handleWheel = useCallback((event: React.WheelEvent) => {
44
+ event.preventDefault()
45
+
46
+ const delta = -event.deltaY / 1000
47
+ const currentScale = scale.get()
48
+ const newScale = Math.min(Math.max(currentScale + delta, minZoom), maxZoom)
49
+
50
+ animate(scale, newScale, { duration: 0.2 })
51
+ onZoomChange?.(newScale)
52
+ }, [scale, minZoom, maxZoom, onZoomChange])
53
+
54
+ const handleDoubleClick = useCallback((event: React.MouseEvent) => {
55
+ event.preventDefault()
56
+
57
+ const currentScale = scale.get()
58
+ const newScale = currentScale > 1 ? 1 : 2
59
+
60
+ animate(scale, Math.min(Math.max(newScale, minZoom), maxZoom), {
61
+ duration: 0.3,
62
+ type: "spring",
63
+ stiffness: 300
64
+ })
65
+
66
+ if (newScale === 1) {
67
+ animate(x, 0, { duration: 0.3 })
68
+ animate(y, 0, { duration: 0.3 })
69
+ }
70
+
71
+ onZoomChange?.(newScale)
72
+ }, [scale, x, y, minZoom, maxZoom, onZoomChange])
73
+
74
+ const resetZoom = useCallback(() => {
75
+ animate(scale, initialZoom, { duration: 0.3 })
76
+ animate(x, 0, { duration: 0.3 })
77
+ animate(y, 0, { duration: 0.3 })
78
+ onZoomChange?.(initialZoom)
79
+ }, [scale, x, y, initialZoom, onZoomChange])
80
+
81
+ return (
82
+ <div
83
+ ref={ref}
84
+ className={cn(
85
+ "relative overflow-hidden touch-none select-none",
86
+ "cursor-grab active:cursor-grabbing",
87
+ className
88
+ )}
89
+ onWheel={handleWheel}
90
+ onDoubleClick={handleDoubleClick}
91
+ {...props}
92
+ >
93
+ <motion.div
94
+ ref={containerRef}
95
+ drag
96
+ dragElastic={0}
97
+ dragMomentum={false}
98
+ onDragStart={() => setIsDragging(true)}
99
+ onDragEnd={() => setIsDragging(false)}
100
+ style={{
101
+ scale: constrainedScale,
102
+ x,
103
+ y,
104
+ }}
105
+ className={cn(
106
+ "w-full h-full flex items-center justify-center",
107
+ isDragging && "cursor-grabbing",
108
+ contentClassName
109
+ )}
110
+ >
111
+ {children}
112
+ </motion.div>
113
+
114
+ {/* Reset Button */}
115
+ <button
116
+ onClick={resetZoom}
117
+ className={cn(
118
+ "absolute bottom-4 right-4 px-3 py-1 bg-background/80 backdrop-blur-sm",
119
+ "border rounded-md text-sm hover:bg-background/90 transition-colors",
120
+ "opacity-0 hover:opacity-100 focus:opacity-100"
121
+ )}
122
+ >
123
+ Reset
124
+ </button>
125
+ </div>
126
+ )
127
+ }
128
+ )
129
+
130
+ PinchZoomInternal.displayName = "PinchZoomInternal"
131
+
132
+ export const PinchZoom = React.forwardRef<HTMLDivElement, PinchZoomProps>(
133
+ ({ className, ...props }, ref) => {
134
+ // Check if we're in docs mode or have pro access
135
+ const docsProAccess = { hasAccess: true } // Pro access assumed in package
136
+ const { hasProAccess, isLoading } = useSubscription()
137
+
138
+ // In docs mode, always show the component
139
+ const canShowComponent = docsProAccess.isDocsMode || hasProAccess
140
+
141
+ // If not in docs mode and no pro access, show upgrade prompt
142
+ if (!docsProAccess.isDocsMode && !isLoading && !hasProAccess) {
143
+ return (
144
+ <Card className={cn("w-fit", className)}>
145
+ <CardContent className="py-6 text-center">
146
+ <div className="space-y-4">
147
+ <div className="rounded-full bg-purple-100 dark:bg-purple-900/30 p-3 w-fit mx-auto">
148
+ <Lock className="h-6 w-6 text-purple-600 dark:text-purple-400" />
149
+ </div>
150
+ <div>
151
+ <h3 className="font-semibold text-sm mb-2">Pro Feature</h3>
152
+ <p className="text-muted-foreground text-xs mb-4">
153
+ Pinch Zoom is available exclusively to MoonUI Pro subscribers.
154
+ </p>
155
+ <a href="/pricing">
156
+ <Button size="sm">
157
+ <Sparkles className="mr-2 h-4 w-4" />
158
+ Upgrade to Pro
159
+ </Button>
160
+ </a>
161
+ </div>
162
+ </div>
163
+ </CardContent>
164
+ </Card>
165
+ )
166
+ }
167
+
168
+ return <PinchZoomInternal className={className} ref={ref} {...props} />
169
+ }
170
+ )
171
+
172
+ PinchZoom.displayName = "PinchZoom"
@@ -0,0 +1,443 @@
1
+ // BACKUP - OLD RICH TEXT EDITOR IMPLEMENTATION
2
+ // This was the previous implementation before the advanced Tiptap-based version
3
+ // Kept for reference and potential markdown editor features
4
+
5
+ "use client"
6
+
7
+ import React from 'react'
8
+ import { useDocsProAccess } from '@/components/docs/docs-pro-provider'
9
+ import { useSubscription } from '../../hooks/use-subscription'
10
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './card'
11
+ import { Button } from './button'
12
+ import {
13
+ Bold,
14
+ Italic,
15
+ Underline,
16
+ AlignLeft,
17
+ AlignCenter,
18
+ AlignRight,
19
+ List,
20
+ ListOrdered,
21
+ Quote,
22
+ Link,
23
+ Image,
24
+ Code,
25
+ Undo,
26
+ Redo,
27
+ Type,
28
+ Lock,
29
+ Sparkles
30
+ } from 'lucide-react'
31
+ import { cn } from '../../lib/utils'
32
+
33
+ export interface OldRichTextEditorProps {
34
+ value?: string
35
+ onChange?: (value: string) => void
36
+ placeholder?: string
37
+ className?: string
38
+ disabled?: boolean
39
+ minHeight?: number
40
+ maxHeight?: number
41
+ showToolbar?: boolean
42
+ readonly?: boolean
43
+ autoFocus?: boolean
44
+ features?: {
45
+ bold?: boolean
46
+ italic?: boolean
47
+ underline?: boolean
48
+ heading?: boolean
49
+ lists?: boolean
50
+ link?: boolean
51
+ image?: boolean
52
+ quote?: boolean
53
+ code?: boolean
54
+ undo?: boolean
55
+ redo?: boolean
56
+ ai?: boolean
57
+ }
58
+ height?: number
59
+ }
60
+
61
+ export function OldRichTextEditor({
62
+ value = '',
63
+ onChange,
64
+ placeholder = 'Start typing...',
65
+ className,
66
+ disabled = false,
67
+ minHeight = 200,
68
+ maxHeight = 600,
69
+ showToolbar = true,
70
+ readonly = false,
71
+ autoFocus = false,
72
+ features = {},
73
+ height
74
+ }: OldRichTextEditorProps) {
75
+ // Check if we're in docs mode or have pro access
76
+ const docsProAccess = useDocsProAccess()
77
+ const { hasProAccess, isLoading } = useSubscription()
78
+
79
+ // In docs mode, always show the component
80
+ const canShowComponent = docsProAccess.isDocsMode || hasProAccess
81
+
82
+ // If not in docs mode and no pro access, show upgrade prompt
83
+ if (!docsProAccess.isDocsMode && !isLoading && !hasProAccess) {
84
+ return (
85
+ <Card className={cn("w-full", className)}>
86
+ <CardContent className="py-12 text-center">
87
+ <div className="max-w-md mx-auto space-y-4">
88
+ <div className="rounded-full bg-purple-100 dark:bg-purple-900/30 p-3 w-fit mx-auto">
89
+ <Lock className="h-6 w-6 text-purple-600 dark:text-purple-400" />
90
+ </div>
91
+ <div>
92
+ <h3 className="font-semibold text-lg mb-2">Pro Feature</h3>
93
+ <p className="text-muted-foreground text-sm mb-4">
94
+ Rich Text Editor is available exclusively to MoonUI Pro subscribers.
95
+ </p>
96
+ <div className="flex gap-3 justify-center">
97
+ <a href="/pricing">
98
+ <Button size="sm">
99
+ <Sparkles className="mr-2 h-4 w-4" />
100
+ Upgrade to Pro
101
+ </Button>
102
+ </a>
103
+ </div>
104
+ </div>
105
+ </div>
106
+ </CardContent>
107
+ </Card>
108
+ )
109
+ }
110
+ const [content, setContent] = React.useState(value)
111
+ const [isFormatting, setIsFormatting] = React.useState({
112
+ bold: false,
113
+ italic: false,
114
+ underline: false,
115
+ align: 'left' as 'left' | 'center' | 'right'
116
+ })
117
+ const editorRef = React.useRef<HTMLDivElement>(null)
118
+
119
+ React.useEffect(() => {
120
+ setContent(value)
121
+ }, [value])
122
+
123
+ React.useEffect(() => {
124
+ if (autoFocus && editorRef.current) {
125
+ editorRef.current.focus()
126
+ }
127
+ }, [autoFocus])
128
+
129
+ const handleContentChange = () => {
130
+ if (editorRef.current) {
131
+ const newContent = editorRef.current.innerHTML
132
+ setContent(newContent)
133
+ onChange?.(newContent)
134
+ }
135
+ }
136
+
137
+ const execCommand = (command: string, value: string = '') => {
138
+ document.execCommand(command, false, value)
139
+ editorRef.current?.focus()
140
+ handleContentChange()
141
+ updateFormattingState()
142
+ }
143
+
144
+ const updateFormattingState = () => {
145
+ setIsFormatting({
146
+ bold: document.queryCommandState('bold'),
147
+ italic: document.queryCommandState('italic'),
148
+ underline: document.queryCommandState('underline'),
149
+ align: document.queryCommandValue('justifyLeft') ? 'left' :
150
+ document.queryCommandValue('justifyCenter') ? 'center' :
151
+ document.queryCommandValue('justifyRight') ? 'right' : 'left'
152
+ })
153
+ }
154
+
155
+ const handleSelectionChange = () => {
156
+ updateFormattingState()
157
+ }
158
+
159
+ React.useEffect(() => {
160
+ document.addEventListener('selectionchange', handleSelectionChange)
161
+ return () => {
162
+ document.removeEventListener('selectionchange', handleSelectionChange)
163
+ }
164
+ }, [])
165
+
166
+ const insertLink = () => {
167
+ const url = prompt('Enter URL:')
168
+ if (url) {
169
+ execCommand('createLink', url)
170
+ }
171
+ }
172
+
173
+ const insertImage = () => {
174
+ const url = prompt('Enter image URL:')
175
+ if (url) {
176
+ execCommand('insertImage', url)
177
+ }
178
+ }
179
+
180
+ const formatText = (command: string) => {
181
+ execCommand(command)
182
+ }
183
+
184
+ const alignText = (alignment: 'left' | 'center' | 'right') => {
185
+ const commands = {
186
+ left: 'justifyLeft',
187
+ center: 'justifyCenter',
188
+ right: 'justifyRight'
189
+ }
190
+ execCommand(commands[alignment])
191
+ }
192
+
193
+ const handleKeyDown = (e: React.KeyboardEvent) => {
194
+ if (e.ctrlKey || e.metaKey) {
195
+ switch (e.key) {
196
+ case 'b':
197
+ e.preventDefault()
198
+ formatText('bold')
199
+ break
200
+ case 'i':
201
+ e.preventDefault()
202
+ formatText('italic')
203
+ break
204
+ case 'u':
205
+ e.preventDefault()
206
+ formatText('underline')
207
+ break
208
+ case 'z':
209
+ e.preventDefault()
210
+ if (e.shiftKey) {
211
+ execCommand('redo')
212
+ } else {
213
+ execCommand('undo')
214
+ }
215
+ break
216
+ }
217
+ }
218
+ }
219
+
220
+ return (
221
+ <Card className={cn("w-full", className)}>
222
+ <CardHeader className="pb-3">
223
+ <CardTitle className="flex items-center gap-2">
224
+ <Type className="h-5 w-5" />
225
+ Rich Text Editor (Old Version)
226
+ </CardTitle>
227
+ <CardDescription>
228
+ Create and edit rich text content with formatting options
229
+ </CardDescription>
230
+ </CardHeader>
231
+ <CardContent className="space-y-4">
232
+ {/* Toolbar */}
233
+ {showToolbar && (
234
+ <div className="flex flex-wrap items-center gap-1 p-2 border rounded-lg bg-muted/50">
235
+ {/* Text Formatting */}
236
+ <div className="flex items-center gap-1">
237
+ {features.bold !== false && (
238
+ <Button
239
+ variant={isFormatting.bold ? "default" : "ghost"}
240
+ size="sm"
241
+ onClick={() => formatText('bold')}
242
+ disabled={disabled || readonly}
243
+ >
244
+ <Bold className="h-4 w-4" />
245
+ </Button>
246
+ )}
247
+ {features.italic !== false && (
248
+ <Button
249
+ variant={isFormatting.italic ? "default" : "ghost"}
250
+ size="sm"
251
+ onClick={() => formatText('italic')}
252
+ disabled={disabled || readonly}
253
+ >
254
+ <Italic className="h-4 w-4" />
255
+ </Button>
256
+ )}
257
+ {features.underline !== false && (
258
+ <Button
259
+ variant={isFormatting.underline ? "default" : "ghost"}
260
+ size="sm"
261
+ onClick={() => formatText('underline')}
262
+ disabled={disabled || readonly}
263
+ >
264
+ <Underline className="h-4 w-4" />
265
+ </Button>
266
+ )}
267
+ </div>
268
+
269
+ <div className="h-4 w-px bg-border mx-1" />
270
+
271
+ {/* Alignment */}
272
+ <div className="flex items-center gap-1">
273
+ <Button
274
+ variant={isFormatting.align === 'left' ? "default" : "ghost"}
275
+ size="sm"
276
+ onClick={() => alignText('left')}
277
+ disabled={disabled || readonly}
278
+ >
279
+ <AlignLeft className="h-4 w-4" />
280
+ </Button>
281
+ <Button
282
+ variant={isFormatting.align === 'center' ? "default" : "ghost"}
283
+ size="sm"
284
+ onClick={() => alignText('center')}
285
+ disabled={disabled || readonly}
286
+ >
287
+ <AlignCenter className="h-4 w-4" />
288
+ </Button>
289
+ <Button
290
+ variant={isFormatting.align === 'right' ? "default" : "ghost"}
291
+ size="sm"
292
+ onClick={() => alignText('right')}
293
+ disabled={disabled || readonly}
294
+ >
295
+ <AlignRight className="h-4 w-4" />
296
+ </Button>
297
+ </div>
298
+
299
+ <div className="h-4 w-px bg-border mx-1" />
300
+
301
+ {/* Lists */}
302
+ {features.lists !== false && (
303
+ <div className="flex items-center gap-1">
304
+ <Button
305
+ variant="ghost"
306
+ size="sm"
307
+ onClick={() => execCommand('insertUnorderedList')}
308
+ disabled={disabled || readonly}
309
+ >
310
+ <List className="h-4 w-4" />
311
+ </Button>
312
+ <Button
313
+ variant="ghost"
314
+ size="sm"
315
+ onClick={() => execCommand('insertOrderedList')}
316
+ disabled={disabled || readonly}
317
+ >
318
+ <ListOrdered className="h-4 w-4" />
319
+ </Button>
320
+ </div>
321
+ )}
322
+
323
+ <div className="h-4 w-px bg-border mx-1" />
324
+
325
+ {/* Quote and Code */}
326
+ <div className="flex items-center gap-1">
327
+ {features.quote !== false && (
328
+ <Button
329
+ variant="ghost"
330
+ size="sm"
331
+ onClick={() => execCommand('formatBlock', 'blockquote')}
332
+ disabled={disabled || readonly}
333
+ >
334
+ <Quote className="h-4 w-4" />
335
+ </Button>
336
+ )}
337
+ {features.code !== false && (
338
+ <Button
339
+ variant="ghost"
340
+ size="sm"
341
+ onClick={() => execCommand('formatBlock', 'pre')}
342
+ disabled={disabled || readonly}
343
+ >
344
+ <Code className="h-4 w-4" />
345
+ </Button>
346
+ )}
347
+ </div>
348
+
349
+ <div className="h-4 w-px bg-border mx-1" />
350
+
351
+ {/* Insert */}
352
+ <div className="flex items-center gap-1">
353
+ {features.link !== false && (
354
+ <Button
355
+ variant="ghost"
356
+ size="sm"
357
+ onClick={insertLink}
358
+ disabled={disabled || readonly}
359
+ >
360
+ <Link className="h-4 w-4" />
361
+ </Button>
362
+ )}
363
+ {features.image !== false && (
364
+ <Button
365
+ variant="ghost"
366
+ size="sm"
367
+ onClick={insertImage}
368
+ disabled={disabled || readonly}
369
+ >
370
+ <Image className="h-4 w-4" />
371
+ </Button>
372
+ )}
373
+ </div>
374
+
375
+ <div className="h-4 w-px bg-border mx-1" />
376
+
377
+ {/* Undo/Redo */}
378
+ {(features.undo !== false || features.redo !== false) && (
379
+ <div className="flex items-center gap-1">
380
+ {features.undo !== false && (
381
+ <Button
382
+ variant="ghost"
383
+ size="sm"
384
+ onClick={() => execCommand('undo')}
385
+ disabled={disabled || readonly}
386
+ >
387
+ <Undo className="h-4 w-4" />
388
+ </Button>
389
+ )}
390
+ {features.redo !== false && (
391
+ <Button
392
+ variant="ghost"
393
+ size="sm"
394
+ onClick={() => execCommand('redo')}
395
+ disabled={disabled || readonly}
396
+ >
397
+ <Redo className="h-4 w-4" />
398
+ </Button>
399
+ )}
400
+ </div>
401
+ )}
402
+ </div>
403
+ )}
404
+
405
+ {/* Editor */}
406
+ <div className="border rounded-lg">
407
+ <div
408
+ ref={editorRef}
409
+ contentEditable={!disabled && !readonly}
410
+ onInput={handleContentChange}
411
+ onKeyDown={handleKeyDown}
412
+ className={cn(
413
+ "w-full p-4 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 rounded-lg",
414
+ "prose prose-sm max-w-none",
415
+ disabled && "cursor-not-allowed opacity-50",
416
+ readonly && "cursor-default"
417
+ )}
418
+ style={{
419
+ minHeight: height ? `${height}px` : `${minHeight}px`,
420
+ maxHeight: height ? `${height}px` : `${maxHeight}px`,
421
+ overflowY: 'auto'
422
+ }}
423
+ dangerouslySetInnerHTML={{ __html: content }}
424
+ data-placeholder={placeholder}
425
+ suppressContentEditableWarning={true}
426
+ />
427
+ </div>
428
+
429
+ {/* Status */}
430
+ <div className="flex items-center justify-between text-xs text-muted-foreground">
431
+ <span>
432
+ {content.replace(/<[^>]*>/g, '').length} characters
433
+ </span>
434
+ <span>
435
+ Press Ctrl+B for bold, Ctrl+I for italic, Ctrl+U for underline
436
+ </span>
437
+ </div>
438
+ </CardContent>
439
+ </Card>
440
+ )
441
+ }
442
+
443
+ export default OldRichTextEditor