@moontra/moonui 2.0.7 → 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.
- package/package.json +26 -19
- 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/ui/__tests__/accordion.test.tsx +571 -0
- package/src/components/ui/__tests__/alert.test.tsx +484 -0
- package/src/components/ui/__tests__/aspect-ratio.test.tsx +492 -0
- package/src/components/ui/__tests__/avatar.test.tsx +382 -0
- package/src/components/ui/__tests__/badge.test.tsx +364 -0
- package/src/components/ui/__tests__/breadcrumb.test.tsx +687 -0
- package/src/components/ui/__tests__/button.test.tsx +91 -0
- package/src/components/ui/__tests__/card.test.tsx +104 -0
- package/src/components/ui/__tests__/checkbox.test.tsx +591 -0
- package/src/components/ui/__tests__/collapsible.test.tsx +592 -0
- package/src/components/ui/__tests__/color-picker.test.tsx +365 -0
- package/src/components/ui/__tests__/command.test.tsx +677 -0
- package/src/components/ui/__tests__/data-table.test.tsx +256 -0
- package/src/components/ui/__tests__/date-picker.test.tsx +320 -0
- package/src/components/ui/__tests__/dialog.test.tsx +764 -0
- package/src/components/ui/__tests__/input.test.tsx +318 -0
- package/src/components/ui/__tests__/label.test.tsx +103 -0
- package/src/components/ui/__tests__/popover.test.tsx +637 -0
- package/src/components/ui/__tests__/progress.test.tsx +382 -0
- package/src/components/ui/__tests__/radio-group.test.tsx +555 -0
- package/src/components/ui/__tests__/select.test.tsx +705 -0
- package/src/components/ui/__tests__/skeleton.test.tsx +253 -0
- package/src/components/ui/__tests__/slider.test.tsx +493 -0
- package/src/components/ui/__tests__/switch.test.tsx +372 -0
- package/src/components/ui/__tests__/table.test.tsx +824 -0
- package/src/components/ui/__tests__/tabs.test.tsx +887 -0
- package/src/components/ui/__tests__/toast.test.tsx +458 -0
- package/src/components/ui/__tests__/tooltip.test.tsx +583 -0
- package/src/components/ui/accordion.tsx +63 -0
- package/src/components/ui/alert.tsx +130 -0
- package/src/components/ui/aspect-ratio.tsx +72 -0
- package/src/components/ui/avatar.tsx +135 -0
- package/src/components/ui/badge.tsx +225 -0
- package/src/components/ui/breadcrumb.tsx +207 -0
- package/src/components/ui/button.stories.tsx +162 -0
- package/src/components/ui/button.tsx +221 -0
- package/src/components/ui/calendar.tsx +262 -0
- package/src/components/ui/card.stories.tsx +208 -0
- package/src/components/ui/card.tsx +141 -0
- package/src/components/ui/checkbox.tsx +256 -0
- package/src/components/ui/collapsible.tsx +129 -0
- package/src/components/ui/color-picker.tsx +664 -0
- package/src/components/ui/command.tsx +218 -0
- package/src/components/ui/data-table.stories.tsx +509 -0
- package/src/components/ui/data-table.tsx +623 -0
- package/src/components/ui/date-picker.stories.tsx +321 -0
- package/src/components/ui/date-picker.tsx +603 -0
- package/src/components/ui/dialog.tsx +332 -0
- package/src/components/ui/dropdown-menu.tsx +200 -0
- package/src/components/ui/file-upload.tsx +400 -0
- package/src/components/ui/github-stars.tsx +201 -0
- package/src/components/ui/index.ts +12 -0
- package/src/components/ui/input.stories.tsx +255 -0
- package/src/components/ui/input.tsx +219 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/locked-component.tsx +215 -0
- package/src/components/ui/moon-logo.tsx +97 -0
- package/src/components/ui/pagination.tsx +116 -0
- package/src/components/ui/popover.tsx +183 -0
- package/src/components/ui/progress.tsx +182 -0
- package/src/components/ui/radio-group.tsx +243 -0
- package/src/components/ui/select.tsx +273 -0
- package/src/components/ui/separator.tsx +140 -0
- package/src/components/ui/simple-editor.tsx +652 -0
- package/src/components/ui/skeleton.tsx +351 -0
- package/src/components/ui/slider.tsx +351 -0
- package/src/components/ui/switch.tsx +83 -0
- package/src/components/ui/table.tsx +322 -0
- package/src/components/ui/tabs.tsx +195 -0
- package/src/components/ui/textarea.tsx +46 -0
- package/src/components/ui/toast.tsx +313 -0
- package/src/components/ui/toggle.tsx +47 -0
- package/src/components/ui/tooltip.tsx +152 -0
- package/src/docs/theme-system.md +1194 -0
- package/src/index.tsx +39 -0
- package/src/lib/micro-interactions.ts +255 -0
- package/src/lib/theme.tsx +398 -0
- package/src/lib/utils.ts +69 -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/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
|
@@ -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', ' ')
|
|
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(/ /g, ' ') // Replace with space
|
|
286
|
+
.replace(/&/g, '&') // Replace & with &
|
|
287
|
+
.replace(/</g, '<') // Replace < with <
|
|
288
|
+
.replace(/>/g, '>') // Replace > with >
|
|
289
|
+
.replace(/"/g, '"') // Replace " with "
|
|
290
|
+
.replace(/'/g, "'") // Replace ' 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
|
+
}
|