@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.
- package/dist/index.mjs +215 -214
- package/package.json +4 -2
- package/src/__tests__/use-intersection-observer.test.tsx +216 -0
- package/src/__tests__/use-local-storage.test.tsx +174 -0
- package/src/__tests__/use-pro-access.test.tsx +183 -0
- package/src/components/advanced-chart/advanced-chart.test.tsx +281 -0
- package/src/components/advanced-chart/index.tsx +412 -0
- package/src/components/advanced-forms/index.tsx +431 -0
- package/src/components/animated-button/index.tsx +202 -0
- package/src/components/calendar/event-dialog.tsx +372 -0
- package/src/components/calendar/index.tsx +557 -0
- package/src/components/color-picker/index.tsx +434 -0
- package/src/components/dashboard/index.tsx +334 -0
- package/src/components/data-table/data-table.test.tsx +187 -0
- package/src/components/data-table/index.tsx +368 -0
- package/src/components/draggable-list/index.tsx +100 -0
- package/src/components/enhanced/button.tsx +360 -0
- package/src/components/enhanced/card.tsx +272 -0
- package/src/components/enhanced/dialog.tsx +248 -0
- package/src/components/enhanced/index.ts +3 -0
- package/src/components/error-boundary/index.tsx +111 -0
- package/src/components/file-upload/file-upload.test.tsx +242 -0
- package/src/components/file-upload/index.tsx +362 -0
- package/src/components/floating-action-button/index.tsx +209 -0
- package/src/components/github-stars/index.tsx +414 -0
- package/src/components/health-check/index.tsx +441 -0
- package/src/components/hover-card-3d/index.tsx +170 -0
- package/src/components/index.ts +76 -0
- package/src/components/kanban/index.tsx +436 -0
- package/src/components/lazy-component/index.tsx +342 -0
- package/src/components/magnetic-button/index.tsx +170 -0
- package/src/components/memory-efficient-data/index.tsx +352 -0
- package/src/components/optimized-image/index.tsx +427 -0
- package/src/components/performance-debugger/index.tsx +591 -0
- package/src/components/performance-monitor/index.tsx +775 -0
- package/src/components/pinch-zoom/index.tsx +172 -0
- package/src/components/rich-text-editor/index-old-backup.tsx +443 -0
- package/src/components/rich-text-editor/index.tsx +1537 -0
- package/src/components/rich-text-editor/slash-commands-extension.ts +220 -0
- package/src/components/rich-text-editor/slash-commands.css +35 -0
- package/src/components/rich-text-editor/table-styles.css +65 -0
- package/src/components/spotlight-card/index.tsx +194 -0
- package/src/components/swipeable-card/index.tsx +100 -0
- package/src/components/timeline/index.tsx +333 -0
- package/src/components/ui/animated-button.tsx +185 -0
- package/src/components/ui/avatar.tsx +135 -0
- package/src/components/ui/badge.tsx +225 -0
- package/src/components/ui/button.tsx +221 -0
- package/src/components/ui/card.tsx +141 -0
- package/src/components/ui/checkbox.tsx +256 -0
- package/src/components/ui/color-picker.tsx +95 -0
- package/src/components/ui/dialog.tsx +332 -0
- package/src/components/ui/dropdown-menu.tsx +200 -0
- package/src/components/ui/hover-card-3d.tsx +103 -0
- package/src/components/ui/index.ts +33 -0
- package/src/components/ui/input.tsx +219 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/magnetic-button.tsx +129 -0
- package/src/components/ui/popover.tsx +183 -0
- package/src/components/ui/select.tsx +273 -0
- package/src/components/ui/separator.tsx +140 -0
- package/src/components/ui/slider.tsx +351 -0
- package/src/components/ui/spotlight-card.tsx +119 -0
- package/src/components/ui/switch.tsx +83 -0
- package/src/components/ui/tabs.tsx +195 -0
- package/src/components/ui/textarea.tsx +25 -0
- package/src/components/ui/toast.tsx +313 -0
- package/src/components/ui/tooltip.tsx +152 -0
- package/src/components/virtual-list/index.tsx +369 -0
- package/src/hooks/use-chart.ts +205 -0
- package/src/hooks/use-data-table.ts +182 -0
- package/src/hooks/use-docs-pro-access.ts +13 -0
- package/src/hooks/use-license-check.ts +65 -0
- package/src/hooks/use-subscription.ts +19 -0
- package/src/index.ts +14 -0
- package/src/lib/micro-interactions.ts +255 -0
- package/src/lib/utils.ts +6 -0
- package/src/patterns/login-form/index.tsx +276 -0
- package/src/patterns/login-form/types.ts +67 -0
- package/src/setupTests.ts +41 -0
- package/src/styles/design-system.css +365 -0
- package/src/styles/index.css +4 -0
- package/src/styles/tailwind.css +6 -0
- package/src/styles/tokens.css +453 -0
- package/src/types/moonui.d.ts +22 -0
- package/src/use-intersection-observer.tsx +154 -0
- package/src/use-local-storage.tsx +71 -0
- package/src/use-paddle.ts +138 -0
- package/src/use-performance-optimizer.ts +379 -0
- package/src/use-pro-access.ts +141 -0
- package/src/use-scroll-animation.ts +221 -0
- package/src/use-subscription.ts +37 -0
- package/src/use-toast.ts +32 -0
- package/src/utils/chart-helpers.ts +257 -0
- package/src/utils/cn.ts +69 -0
- package/src/utils/data-processing.ts +151 -0
- package/src/utils/license-guard.tsx +177 -0
- package/src/utils/license-validator.tsx +183 -0
- 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
|