@betterstart/cli 0.1.3 → 0.1.4
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/README.md +133 -0
- package/dist/cli.d.ts +1 -9
- package/dist/cli.js +13484 -367
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +24 -266
- package/dist/index.js +4 -11378
- package/dist/index.js.map +1 -1
- package/package.json +29 -42
- package/templates/schema.json +959 -0
- package/templates/tiptap/hooks/use-composed-ref.ts +43 -0
- package/templates/tiptap/hooks/use-cursor-visibility.ts +68 -0
- package/templates/tiptap/hooks/use-element-rect.ts +166 -0
- package/templates/tiptap/hooks/use-is-breakpoint.ts +32 -0
- package/templates/tiptap/hooks/use-menu-navigation.ts +182 -0
- package/templates/tiptap/hooks/use-scrolling.ts +64 -0
- package/templates/tiptap/hooks/use-throttled-callback.ts +146 -0
- package/templates/tiptap/hooks/use-tiptap-editor.ts +46 -0
- package/templates/tiptap/hooks/use-unmount.ts +21 -0
- package/templates/tiptap/hooks/use-window-size.ts +87 -0
- package/templates/tiptap/lib/tiptap-utils.ts +587 -0
- package/templates/tiptap/styles/_keyframe-animations.scss +91 -0
- package/templates/tiptap/styles/_variables.scss +296 -0
- package/templates/tiptap/tiptap-extension/node-background-extension.ts +138 -0
- package/templates/tiptap/tiptap-icons/align-center-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/align-justify-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/align-left-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/align-right-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/arrow-left-icon.tsx +24 -0
- package/templates/tiptap/tiptap-icons/ban-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/blockquote-icon.tsx +44 -0
- package/templates/tiptap/tiptap-icons/bold-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/chevron-down-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/close-icon.tsx +24 -0
- package/templates/tiptap/tiptap-icons/code-block-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/code2-icon.tsx +32 -0
- package/templates/tiptap/tiptap-icons/corner-down-left-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/external-link-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/heading-five-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/heading-four-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/heading-icon.tsx +24 -0
- package/templates/tiptap/tiptap-icons/heading-one-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/heading-six-icon.tsx +30 -0
- package/templates/tiptap/tiptap-icons/heading-three-icon.tsx +36 -0
- package/templates/tiptap/tiptap-icons/heading-two-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/highlighter-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/image-plus-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/italic-icon.tsx +24 -0
- package/templates/tiptap/tiptap-icons/link-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/list-icon.tsx +56 -0
- package/templates/tiptap/tiptap-icons/list-ordered-icon.tsx +56 -0
- package/templates/tiptap/tiptap-icons/list-todo-icon.tsx +50 -0
- package/templates/tiptap/tiptap-icons/moon-star-icon.tsx +30 -0
- package/templates/tiptap/tiptap-icons/redo2-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/strike-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/subscript-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/sun-icon.tsx +58 -0
- package/templates/tiptap/tiptap-icons/superscript-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/trash-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/underline-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/undo2-icon.tsx +26 -0
- package/templates/tiptap/tiptap-node/blockquote-node/blockquote-node.scss +37 -0
- package/templates/tiptap/tiptap-node/code-block-node/code-block-node.scss +54 -0
- package/templates/tiptap/tiptap-node/heading-node/heading-node.scss +45 -0
- package/templates/tiptap/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension.ts +10 -0
- package/templates/tiptap/tiptap-node/horizontal-rule-node/horizontal-rule-node.scss +25 -0
- package/templates/tiptap/tiptap-node/image-node/image-node.scss +35 -0
- package/templates/tiptap/tiptap-node/image-upload-node/image-upload-node-extension.ts +154 -0
- package/templates/tiptap/tiptap-node/image-upload-node/image-upload-node.scss +249 -0
- package/templates/tiptap/tiptap-node/image-upload-node/image-upload-node.tsx +522 -0
- package/templates/tiptap/tiptap-node/image-upload-node/index.tsx +1 -0
- package/templates/tiptap/tiptap-node/list-node/list-node.scss +208 -0
- package/templates/tiptap/tiptap-node/paragraph-node/paragraph-node.scss +273 -0
- package/templates/tiptap/tiptap-ui/blockquote-button/blockquote-button.tsx +104 -0
- package/templates/tiptap/tiptap-ui/blockquote-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/blockquote-button/use-blockquote.ts +252 -0
- package/templates/tiptap/tiptap-ui/code-block-button/code-block-button.tsx +106 -0
- package/templates/tiptap/tiptap-ui/code-block-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/code-block-button/use-code-block.ts +261 -0
- package/templates/tiptap/tiptap-ui/color-highlight-button/color-highlight-button.scss +49 -0
- package/templates/tiptap/tiptap-ui/color-highlight-button/color-highlight-button.tsx +153 -0
- package/templates/tiptap/tiptap-ui/color-highlight-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/color-highlight-button/use-color-highlight.ts +345 -0
- package/templates/tiptap/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx +207 -0
- package/templates/tiptap/tiptap-ui/color-highlight-popover/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui/heading-button/heading-button.tsx +107 -0
- package/templates/tiptap/tiptap-ui/heading-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/heading-button/use-heading.ts +314 -0
- package/templates/tiptap/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx +131 -0
- package/templates/tiptap/tiptap-ui/heading-dropdown-menu/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts +130 -0
- package/templates/tiptap/tiptap-ui/image-upload-button/image-upload-button.tsx +114 -0
- package/templates/tiptap/tiptap-ui/image-upload-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/image-upload-button/use-image-upload.ts +192 -0
- package/templates/tiptap/tiptap-ui/link-popover/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/link-popover/link-popover.tsx +285 -0
- package/templates/tiptap/tiptap-ui/link-popover/use-link-popover.ts +286 -0
- package/templates/tiptap/tiptap-ui/list-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/list-button/list-button.tsx +108 -0
- package/templates/tiptap/tiptap-ui/list-button/use-list.ts +329 -0
- package/templates/tiptap/tiptap-ui/list-dropdown-menu/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx +123 -0
- package/templates/tiptap/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts +203 -0
- package/templates/tiptap/tiptap-ui/mark-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/mark-button/mark-button.tsx +107 -0
- package/templates/tiptap/tiptap-ui/mark-button/use-mark.ts +206 -0
- package/templates/tiptap/tiptap-ui/text-align-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/text-align-button/text-align-button.tsx +118 -0
- package/templates/tiptap/tiptap-ui/text-align-button/use-text-align.ts +212 -0
- package/templates/tiptap/tiptap-ui/undo-redo-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/undo-redo-button/undo-redo-button.tsx +105 -0
- package/templates/tiptap/tiptap-ui/undo-redo-button/use-undo-redo.ts +173 -0
- package/templates/tiptap/tiptap-ui-primitive/badge/badge-colors.scss +395 -0
- package/templates/tiptap/tiptap-ui-primitive/badge/badge-group.scss +16 -0
- package/templates/tiptap/tiptap-ui-primitive/badge/badge.scss +99 -0
- package/templates/tiptap/tiptap-ui-primitive/badge/badge.tsx +46 -0
- package/templates/tiptap/tiptap-ui-primitive/badge/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/button/button-colors.scss +429 -0
- package/templates/tiptap/tiptap-ui-primitive/button/button-group.scss +22 -0
- package/templates/tiptap/tiptap-ui-primitive/button/button.scss +314 -0
- package/templates/tiptap/tiptap-ui-primitive/button/button.tsx +102 -0
- package/templates/tiptap/tiptap-ui-primitive/button/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/card/card.scss +77 -0
- package/templates/tiptap/tiptap-ui-primitive/card/card.tsx +59 -0
- package/templates/tiptap/tiptap-ui-primitive/card/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/dropdown-menu/dropdown-menu.scss +63 -0
- package/templates/tiptap/tiptap-ui-primitive/dropdown-menu/dropdown-menu.tsx +95 -0
- package/templates/tiptap/tiptap-ui-primitive/dropdown-menu/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/input/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/input/input.scss +45 -0
- package/templates/tiptap/tiptap-ui-primitive/input/input.tsx +18 -0
- package/templates/tiptap/tiptap-ui-primitive/popover/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/popover/popover.scss +63 -0
- package/templates/tiptap/tiptap-ui-primitive/popover/popover.tsx +33 -0
- package/templates/tiptap/tiptap-ui-primitive/separator/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/separator/separator.scss +23 -0
- package/templates/tiptap/tiptap-ui-primitive/separator/separator.tsx +33 -0
- package/templates/tiptap/tiptap-ui-primitive/spacer/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/spacer/spacer.tsx +21 -0
- package/templates/tiptap/tiptap-ui-primitive/toolbar/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/toolbar/toolbar.scss +98 -0
- package/templates/tiptap/tiptap-ui-primitive/toolbar/toolbar.tsx +113 -0
- package/templates/tiptap/tiptap-ui-primitive/tooltip/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/tooltip/tooltip.scss +43 -0
- package/templates/tiptap/tiptap-ui-primitive/tooltip/tooltip.tsx +223 -0
- package/templates/ui/accordion.tsx +52 -0
- package/templates/ui/alert-dialog.tsx +116 -0
- package/templates/ui/alert.tsx +48 -0
- package/templates/ui/aspect-ratio.tsx +7 -0
- package/templates/ui/avatar.tsx +46 -0
- package/templates/ui/badge.tsx +32 -0
- package/templates/ui/breadcrumb.tsx +98 -0
- package/templates/ui/button-group.tsx +77 -0
- package/templates/ui/button.tsx +48 -0
- package/templates/ui/calendar.tsx +176 -0
- package/templates/ui/card.tsx +54 -0
- package/templates/ui/carousel.tsx +234 -0
- package/templates/ui/chart.tsx +349 -0
- package/templates/ui/checkbox.tsx +27 -0
- package/templates/ui/collapsible.tsx +11 -0
- package/templates/ui/command.tsx +142 -0
- package/templates/ui/context-menu.tsx +188 -0
- package/templates/ui/curriculum-editor.tsx +601 -0
- package/templates/ui/date-picker.tsx +70 -0
- package/templates/ui/dialog.tsx +103 -0
- package/templates/ui/drawer.tsx +99 -0
- package/templates/ui/dropdown-menu.tsx +185 -0
- package/templates/ui/dynamic-list-field.tsx +95 -0
- package/templates/ui/empty.tsx +90 -0
- package/templates/ui/field.tsx +231 -0
- package/templates/ui/file-upload-example.tsx +113 -0
- package/templates/ui/form.tsx +172 -0
- package/templates/ui/hover-card.tsx +28 -0
- package/templates/ui/icon-picker.tsx +435 -0
- package/templates/ui/icons-data.ts +6 -0
- package/templates/ui/image-upload-field.tsx +360 -0
- package/templates/ui/input-group.tsx +160 -0
- package/templates/ui/input-otp.tsx +70 -0
- package/templates/ui/input.tsx +21 -0
- package/templates/ui/item.tsx +171 -0
- package/templates/ui/kbd.tsx +28 -0
- package/templates/ui/label.tsx +20 -0
- package/templates/ui/logo.tsx +113 -0
- package/templates/ui/markdown-editor.tsx +303 -0
- package/templates/ui/markdown-utils.ts +128 -0
- package/templates/ui/media-upload-field.tsx +255 -0
- package/templates/ui/menubar.tsx +230 -0
- package/templates/ui/navigation-menu.tsx +119 -0
- package/templates/ui/pagination.tsx +96 -0
- package/templates/ui/placeholder.tsx +25 -0
- package/templates/ui/popover.tsx +32 -0
- package/templates/ui/progress.tsx +24 -0
- package/templates/ui/radio-group.tsx +37 -0
- package/templates/ui/resizable.tsx +41 -0
- package/templates/ui/rich-text-editor.tsx +374 -0
- package/templates/ui/scroll-area.tsx +45 -0
- package/templates/ui/select.tsx +151 -0
- package/templates/ui/separator.tsx +25 -0
- package/templates/ui/sheet.tsx +120 -0
- package/templates/ui/sidebar.tsx +684 -0
- package/templates/ui/skeleton.tsx +7 -0
- package/templates/ui/slider.tsx +24 -0
- package/templates/ui/sonner.tsx +29 -0
- package/templates/ui/spinner.tsx +15 -0
- package/templates/ui/switch.tsx +28 -0
- package/templates/ui/table.tsx +93 -0
- package/templates/ui/tabs.tsx +54 -0
- package/templates/ui/textarea.tsx +20 -0
- package/templates/ui/toast.tsx +127 -0
- package/templates/ui/toggle-group.tsx +56 -0
- package/templates/ui/toggle.tsx +43 -0
- package/templates/ui/tooltip.tsx +31 -0
- package/templates/ui/use-mobile.tsx +19 -0
- package/templates/ui/video-upload-field.tsx +368 -0
- package/dist/chunk-EIH4RRIJ.js +0 -183
- package/dist/chunk-EIH4RRIJ.js.map +0 -1
- package/dist/chunk-NKRQYAS6.js +0 -260
- package/dist/chunk-NKRQYAS6.js.map +0 -1
- package/dist/chunk-QLVSHP7X.js +0 -235
- package/dist/chunk-QLVSHP7X.js.map +0 -1
- package/dist/chunk-WY6BC55D.js +0 -357
- package/dist/chunk-WY6BC55D.js.map +0 -1
- package/dist/config/index.d.ts +0 -93
- package/dist/config/index.js +0 -58
- package/dist/config/index.js.map +0 -1
- package/dist/core/index.d.ts +0 -415
- package/dist/core/index.js +0 -906
- package/dist/core/index.js.map +0 -1
- package/dist/import-resolver-BaZ-rzkH.d.ts +0 -123
- package/dist/logger-awLb347n.d.ts +0 -81
- package/dist/plugins/index.d.ts +0 -213
- package/dist/plugins/index.js +0 -365
- package/dist/plugins/index.js.map +0 -1
- package/dist/types-ByX_gl6y.d.ts +0 -232
- package/dist/types-eI549DEG.d.ts +0 -331
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useUpload } from '@cms/hooks/use-upload'
|
|
4
|
+
import { cn } from '@cms/utils/cn'
|
|
5
|
+
import { Check, Loader2, Upload, X } from 'lucide-react'
|
|
6
|
+
import * as React from 'react'
|
|
7
|
+
import { toast } from 'sonner'
|
|
8
|
+
import { Button } from './button'
|
|
9
|
+
import { Input } from './input'
|
|
10
|
+
import { Progress } from './progress'
|
|
11
|
+
|
|
12
|
+
function isValidUrl(url: string): boolean {
|
|
13
|
+
try {
|
|
14
|
+
new URL(url)
|
|
15
|
+
return true
|
|
16
|
+
} catch {
|
|
17
|
+
return false
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createPreviewUrl(file: File): string {
|
|
22
|
+
return URL.createObjectURL(file)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function revokePreviewUrl(url: string): void {
|
|
26
|
+
if (url.startsWith('blob:')) URL.revokeObjectURL(url)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function useFetchImage({ maxSizeInMB }: { accept: string; maxSizeInMB: number }) {
|
|
30
|
+
const [isFetching, setIsFetching] = React.useState(false)
|
|
31
|
+
|
|
32
|
+
const fetchImageFromUrl = React.useCallback(
|
|
33
|
+
async (url: string): Promise<File | null> => {
|
|
34
|
+
setIsFetching(true)
|
|
35
|
+
try {
|
|
36
|
+
const response = await fetch(url)
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
toast.error('Failed to fetch image from URL')
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
const blob = await response.blob()
|
|
42
|
+
if (blob.size > maxSizeInMB * 1024 * 1024) {
|
|
43
|
+
toast.error(`Image exceeds ${maxSizeInMB}MB limit`)
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
const filename = url.split('/').pop() || 'image'
|
|
47
|
+
return new File([blob], filename, { type: blob.type })
|
|
48
|
+
} catch {
|
|
49
|
+
toast.error('Failed to fetch image from URL')
|
|
50
|
+
return null
|
|
51
|
+
} finally {
|
|
52
|
+
setIsFetching(false)
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
[maxSizeInMB]
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return { fetchImageFromUrl, isFetching }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ImageUploadFieldProps {
|
|
62
|
+
value?: string
|
|
63
|
+
onChange: (value: string) => void
|
|
64
|
+
onBlur?: () => void
|
|
65
|
+
disabled?: boolean
|
|
66
|
+
accept?: string
|
|
67
|
+
maxSizeInMB?: number
|
|
68
|
+
className?: string
|
|
69
|
+
label?: string
|
|
70
|
+
description?: string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface UrlInputState {
|
|
74
|
+
value: string
|
|
75
|
+
preview: string | null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Image upload field component with R2 integration
|
|
80
|
+
* Designed for use in forms with react-hook-form
|
|
81
|
+
*/
|
|
82
|
+
export function ImageUploadField({
|
|
83
|
+
value,
|
|
84
|
+
onChange,
|
|
85
|
+
onBlur,
|
|
86
|
+
disabled = false,
|
|
87
|
+
accept = 'image/*',
|
|
88
|
+
maxSizeInMB = 5,
|
|
89
|
+
className,
|
|
90
|
+
label,
|
|
91
|
+
description
|
|
92
|
+
}: ImageUploadFieldProps) {
|
|
93
|
+
const fileInputRef = React.useRef<HTMLInputElement>(null)
|
|
94
|
+
const [previewUrl, setPreviewUrl] = React.useState<string | null>(value || null)
|
|
95
|
+
const [urlState, setUrlState] = React.useState<UrlInputState>({
|
|
96
|
+
value: '',
|
|
97
|
+
preview: null
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const { fetchImageFromUrl, isFetching: isFetchingUrl } = useFetchImage({
|
|
101
|
+
accept,
|
|
102
|
+
maxSizeInMB
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const { upload, mutation, progress } = useUpload({
|
|
106
|
+
accept,
|
|
107
|
+
maxSizeInMB,
|
|
108
|
+
validationConfig: {
|
|
109
|
+
maxFiles: 1
|
|
110
|
+
},
|
|
111
|
+
onSuccess: (result) => {
|
|
112
|
+
if (result.success && result.files?.[0]) {
|
|
113
|
+
const uploadedUrl = result.files[0].url
|
|
114
|
+
onChange(uploadedUrl)
|
|
115
|
+
setPreviewUrl(uploadedUrl)
|
|
116
|
+
toast.success('Image uploaded successfully')
|
|
117
|
+
} else {
|
|
118
|
+
const errorMsg = result.error || 'Upload failed'
|
|
119
|
+
toast.error(errorMsg)
|
|
120
|
+
setPreviewUrl(value || null)
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
onError: (error) => {
|
|
124
|
+
toast.error(error.message || 'Network error during upload')
|
|
125
|
+
setPreviewUrl(value || null)
|
|
126
|
+
},
|
|
127
|
+
prefix: 'images'
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
// Update preview when value changes externally
|
|
131
|
+
React.useEffect(() => {
|
|
132
|
+
if (value) {
|
|
133
|
+
setPreviewUrl(value)
|
|
134
|
+
}
|
|
135
|
+
}, [value])
|
|
136
|
+
|
|
137
|
+
// Cleanup object URLs on unmount
|
|
138
|
+
React.useEffect(() => {
|
|
139
|
+
return () => {
|
|
140
|
+
if (previewUrl) {
|
|
141
|
+
revokePreviewUrl(previewUrl)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}, [previewUrl])
|
|
145
|
+
|
|
146
|
+
const handleFileSelect = React.useCallback(
|
|
147
|
+
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
148
|
+
const file = e.target.files?.[0]
|
|
149
|
+
if (!file) return
|
|
150
|
+
|
|
151
|
+
if (file.size === 0) {
|
|
152
|
+
toast.error('Cannot upload empty file')
|
|
153
|
+
if (fileInputRef.current) {
|
|
154
|
+
fileInputRef.current.value = ''
|
|
155
|
+
}
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Create local preview
|
|
160
|
+
try {
|
|
161
|
+
const localPreview = createPreviewUrl(file)
|
|
162
|
+
setPreviewUrl(localPreview)
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error('[ImageUploadField] Failed to create preview:', error)
|
|
165
|
+
toast.error('Failed to preview image')
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
upload([file])
|
|
170
|
+
},
|
|
171
|
+
[upload]
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
const handleUrlChange = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
175
|
+
const url = e.target.value.trim()
|
|
176
|
+
const preview = url && isValidUrl(url) ? url : null
|
|
177
|
+
setUrlState({ value: url, preview })
|
|
178
|
+
}, [])
|
|
179
|
+
|
|
180
|
+
const handleUrlConfirm = React.useCallback(async () => {
|
|
181
|
+
if (!urlState.value.trim()) {
|
|
182
|
+
toast.error('Please enter a valid image URL')
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const file = await fetchImageFromUrl(urlState.value.trim())
|
|
187
|
+
if (!file) return
|
|
188
|
+
|
|
189
|
+
// Create local preview
|
|
190
|
+
try {
|
|
191
|
+
const localPreview = createPreviewUrl(file)
|
|
192
|
+
setPreviewUrl(localPreview)
|
|
193
|
+
setUrlState({ value: '', preview: null })
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.error('[ImageUploadField] Failed to create preview:', error)
|
|
196
|
+
toast.error('Failed to preview image')
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
upload([file])
|
|
201
|
+
}, [urlState.value, fetchImageFromUrl, upload])
|
|
202
|
+
|
|
203
|
+
const handleRemove = React.useCallback(() => {
|
|
204
|
+
if (previewUrl) {
|
|
205
|
+
revokePreviewUrl(previewUrl)
|
|
206
|
+
}
|
|
207
|
+
setPreviewUrl(null)
|
|
208
|
+
setUrlState({ value: '', preview: null })
|
|
209
|
+
onChange('')
|
|
210
|
+
mutation.reset()
|
|
211
|
+
if (fileInputRef.current) {
|
|
212
|
+
fileInputRef.current.value = ''
|
|
213
|
+
}
|
|
214
|
+
}, [previewUrl, onChange, mutation])
|
|
215
|
+
|
|
216
|
+
const handleClick = React.useCallback(() => {
|
|
217
|
+
fileInputRef.current?.click()
|
|
218
|
+
}, [])
|
|
219
|
+
|
|
220
|
+
const isUploading = mutation.isPending
|
|
221
|
+
const uploadProgress = progress[0]?.progress || 0
|
|
222
|
+
const displayPreview = urlState.preview || previewUrl
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<div className={cn('space-y-2', className)}>
|
|
226
|
+
{label && (
|
|
227
|
+
<label htmlFor="file-input" className="text-sm font-medium">
|
|
228
|
+
{label}
|
|
229
|
+
</label>
|
|
230
|
+
)}
|
|
231
|
+
{description && <p className="text-sm text-muted-foreground">{description}</p>}
|
|
232
|
+
|
|
233
|
+
<div className="space-y-4">
|
|
234
|
+
<input
|
|
235
|
+
id="file-input"
|
|
236
|
+
ref={fileInputRef}
|
|
237
|
+
type="file"
|
|
238
|
+
accept={accept}
|
|
239
|
+
onChange={handleFileSelect}
|
|
240
|
+
onBlur={onBlur}
|
|
241
|
+
disabled={disabled || isUploading || isFetchingUrl}
|
|
242
|
+
className="hidden"
|
|
243
|
+
/>
|
|
244
|
+
|
|
245
|
+
{/* URL Input Section */}
|
|
246
|
+
<div className="space-y-2">
|
|
247
|
+
<div className="flex gap-2">
|
|
248
|
+
<Input
|
|
249
|
+
type="url"
|
|
250
|
+
placeholder="Paste image URL here or Click below to upload"
|
|
251
|
+
value={urlState.value}
|
|
252
|
+
onChange={handleUrlChange}
|
|
253
|
+
onBlur={onBlur}
|
|
254
|
+
disabled={disabled || isUploading || isFetchingUrl}
|
|
255
|
+
className="flex-1"
|
|
256
|
+
/>
|
|
257
|
+
{urlState.preview && (
|
|
258
|
+
<Button
|
|
259
|
+
type="button"
|
|
260
|
+
onClick={handleUrlConfirm}
|
|
261
|
+
disabled={disabled || isUploading || isFetchingUrl}
|
|
262
|
+
className="shrink-0"
|
|
263
|
+
size="lg"
|
|
264
|
+
>
|
|
265
|
+
{isFetchingUrl ? (
|
|
266
|
+
<Loader2 className="size-4 animate-spin" />
|
|
267
|
+
) : (
|
|
268
|
+
<>
|
|
269
|
+
<Check className="size-4" />
|
|
270
|
+
Confirm
|
|
271
|
+
</>
|
|
272
|
+
)}
|
|
273
|
+
</Button>
|
|
274
|
+
)}
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
{/* Preview or Upload Area */}
|
|
279
|
+
{displayPreview ? (
|
|
280
|
+
<div className="relative w-full rounded-lg corner-squircle border border-dashed border-border bg-secondary h-50 flex items-center justify-center p-10 group">
|
|
281
|
+
<img
|
|
282
|
+
src={displayPreview}
|
|
283
|
+
alt="Preview"
|
|
284
|
+
className="w-full h-auto max-h-40 object-contain"
|
|
285
|
+
/>
|
|
286
|
+
|
|
287
|
+
{/* Upload Progress Overlay */}
|
|
288
|
+
{isUploading && (
|
|
289
|
+
<div className="absolute inset-0 bg-black/50 flex flex-col items-center justify-center gap-2">
|
|
290
|
+
<Loader2 className="size-8 text-white animate-spin" />
|
|
291
|
+
<Progress value={uploadProgress} className="w-3/4" />
|
|
292
|
+
<span className="text-white text-sm">{uploadProgress}%</span>
|
|
293
|
+
</div>
|
|
294
|
+
)}
|
|
295
|
+
|
|
296
|
+
{/* Remove Button */}
|
|
297
|
+
{!isUploading && !isFetchingUrl && (
|
|
298
|
+
<Button
|
|
299
|
+
type="button"
|
|
300
|
+
variant="destructive"
|
|
301
|
+
size="icon"
|
|
302
|
+
className="absolute top-2 right-2 hidden group-hover:flex"
|
|
303
|
+
onClick={handleRemove}
|
|
304
|
+
disabled={disabled}
|
|
305
|
+
>
|
|
306
|
+
<X className="size-4" />
|
|
307
|
+
</Button>
|
|
308
|
+
)}
|
|
309
|
+
|
|
310
|
+
{/* URL Preview Indicator */}
|
|
311
|
+
{urlState.preview && !previewUrl && (
|
|
312
|
+
<div className="absolute bottom-2 left-2 bg-primary/90 text-primary-foreground text-xs px-2 py-1 rounded">
|
|
313
|
+
Click Confirm to upload
|
|
314
|
+
</div>
|
|
315
|
+
)}
|
|
316
|
+
</div>
|
|
317
|
+
) : (
|
|
318
|
+
<button
|
|
319
|
+
type="button"
|
|
320
|
+
onClick={handleClick}
|
|
321
|
+
disabled={disabled || isUploading}
|
|
322
|
+
className={cn(
|
|
323
|
+
'w-full rounded-lg corner-squircle border border-dashed border-border bg-secondary h-50 flex items-center justify-center p-10 group',
|
|
324
|
+
'hover:border-primary/50 transition-colors',
|
|
325
|
+
'flex flex-col items-center justify-center gap-2 p-8',
|
|
326
|
+
'disabled:opacity-50 disabled:cursor-not-allowed'
|
|
327
|
+
)}
|
|
328
|
+
>
|
|
329
|
+
{isUploading ? (
|
|
330
|
+
<>
|
|
331
|
+
<Loader2 className="size-10 text-muted-foreground animate-spin" />
|
|
332
|
+
<Progress value={uploadProgress} className="w-3/4" />
|
|
333
|
+
<span className="text-sm text-muted-foreground">{uploadProgress}%</span>
|
|
334
|
+
</>
|
|
335
|
+
) : (
|
|
336
|
+
<>
|
|
337
|
+
<div className="rounded-full corner-squircle bg-primary/10 p-3">
|
|
338
|
+
<Upload className="size-6 text-primary" />
|
|
339
|
+
</div>
|
|
340
|
+
<div className="text-center">
|
|
341
|
+
<p className="text-sm font-medium">Click to upload image</p>
|
|
342
|
+
<p className="text-xs text-muted-foreground mt-1">Max size: {maxSizeInMB}MB</p>
|
|
343
|
+
</div>
|
|
344
|
+
</>
|
|
345
|
+
)}
|
|
346
|
+
</button>
|
|
347
|
+
)}
|
|
348
|
+
|
|
349
|
+
{/* Error Message */}
|
|
350
|
+
{mutation.isError && (
|
|
351
|
+
<div className="text-sm text-destructive">Upload failed: {mutation.error.message}</div>
|
|
352
|
+
)}
|
|
353
|
+
|
|
354
|
+
{mutation.data && !mutation.data.success && (
|
|
355
|
+
<div className="text-sm text-destructive">{mutation.data.error}</div>
|
|
356
|
+
)}
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
)
|
|
360
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { cn } from '@cms/utils/cn'
|
|
4
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
5
|
+
import type * as React from 'react'
|
|
6
|
+
import { Button } from './button'
|
|
7
|
+
import { Input } from './input'
|
|
8
|
+
import { Textarea } from './textarea'
|
|
9
|
+
|
|
10
|
+
function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
|
11
|
+
return (
|
|
12
|
+
<div
|
|
13
|
+
data-slot="input-group"
|
|
14
|
+
role="group"
|
|
15
|
+
className={cn(
|
|
16
|
+
'group/input-group border-input-border dark:bg-input/30 shadow-xs relative flex w-full items-center rounded-md border outline-none transition-[color,box-shadow]',
|
|
17
|
+
'h-9 has-[>textarea]:h-auto',
|
|
18
|
+
|
|
19
|
+
// Variants based on alignment.
|
|
20
|
+
'has-[>[data-align=inline-start]]:[&>input]:pl-2',
|
|
21
|
+
'has-[>[data-align=inline-end]]:[&>input]:pr-2',
|
|
22
|
+
'has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3',
|
|
23
|
+
'has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3',
|
|
24
|
+
|
|
25
|
+
// Focus state.
|
|
26
|
+
'has-[[data-slot=input-group-control]:focus-visible]:ring-ring has-[[data-slot=input-group-control]:focus-visible]:ring-1',
|
|
27
|
+
|
|
28
|
+
// Error state.
|
|
29
|
+
'has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40',
|
|
30
|
+
|
|
31
|
+
className
|
|
32
|
+
)}
|
|
33
|
+
{...props}
|
|
34
|
+
/>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const inputGroupAddonVariants = cva(
|
|
39
|
+
"text-muted-foreground flex h-auto cursor-text select-none items-center justify-center gap-2 py-1.5 text-sm font-medium group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
|
|
40
|
+
{
|
|
41
|
+
variants: {
|
|
42
|
+
align: {
|
|
43
|
+
'inline-start': 'order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]',
|
|
44
|
+
'inline-end': 'order-last pr-3 has-[>button]:mr-[-0.4rem] has-[>kbd]:mr-[-0.35rem]',
|
|
45
|
+
'block-start':
|
|
46
|
+
'[.border-b]:pb-3 order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5',
|
|
47
|
+
'block-end':
|
|
48
|
+
'[.border-t]:pt-3 order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5'
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
defaultVariants: {
|
|
52
|
+
align: 'inline-start'
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
function InputGroupAddon({
|
|
58
|
+
className,
|
|
59
|
+
align = 'inline-start',
|
|
60
|
+
onClick,
|
|
61
|
+
...props
|
|
62
|
+
}: React.ComponentProps<'div'> & VariantProps<typeof inputGroupAddonVariants>) {
|
|
63
|
+
return (
|
|
64
|
+
// biome-ignore lint/a11y/noStaticElementInteractions: Click-to-focus is a mouse convenience, keyboard users can tab directly to input
|
|
65
|
+
// biome-ignore lint/a11y/useKeyWithClickEvents: Keyboard users can tab directly to input field
|
|
66
|
+
<div
|
|
67
|
+
data-slot="input-group-addon"
|
|
68
|
+
data-align={align}
|
|
69
|
+
className={cn(inputGroupAddonVariants({ align }), className)}
|
|
70
|
+
onClick={(e) => {
|
|
71
|
+
// Focus input when clicking addon area (but not buttons inside)
|
|
72
|
+
if (!(e.target as HTMLElement).closest('button')) {
|
|
73
|
+
e.currentTarget.parentElement?.querySelector('input')?.focus()
|
|
74
|
+
}
|
|
75
|
+
onClick?.(e)
|
|
76
|
+
}}
|
|
77
|
+
{...props}
|
|
78
|
+
/>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const inputGroupButtonVariants = cva('flex items-center gap-2 text-sm shadow-none', {
|
|
83
|
+
variants: {
|
|
84
|
+
size: {
|
|
85
|
+
xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5",
|
|
86
|
+
sm: 'h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5',
|
|
87
|
+
'icon-xs': 'size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0',
|
|
88
|
+
'icon-sm': 'size-8 p-0 has-[>svg]:p-0'
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
defaultVariants: {
|
|
92
|
+
size: 'xs'
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
function InputGroupButton({
|
|
97
|
+
className,
|
|
98
|
+
type = 'button',
|
|
99
|
+
variant = 'ghost',
|
|
100
|
+
size = 'xs',
|
|
101
|
+
...props
|
|
102
|
+
}: Omit<React.ComponentProps<typeof Button>, 'size'> &
|
|
103
|
+
VariantProps<typeof inputGroupButtonVariants>) {
|
|
104
|
+
return (
|
|
105
|
+
<Button
|
|
106
|
+
type={type}
|
|
107
|
+
data-size={size}
|
|
108
|
+
variant={variant}
|
|
109
|
+
className={cn(inputGroupButtonVariants({ size }), className)}
|
|
110
|
+
{...props}
|
|
111
|
+
/>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function InputGroupText({ className, ...props }: React.ComponentProps<'span'>) {
|
|
116
|
+
return (
|
|
117
|
+
<span
|
|
118
|
+
className={cn(
|
|
119
|
+
"text-muted-foreground flex items-center gap-2 text-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
|
|
120
|
+
className
|
|
121
|
+
)}
|
|
122
|
+
{...props}
|
|
123
|
+
/>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function InputGroupInput({ className, ...props }: React.ComponentProps<'input'>) {
|
|
128
|
+
return (
|
|
129
|
+
<Input
|
|
130
|
+
data-slot="input-group-control"
|
|
131
|
+
className={cn(
|
|
132
|
+
'flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent',
|
|
133
|
+
className
|
|
134
|
+
)}
|
|
135
|
+
{...props}
|
|
136
|
+
/>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function InputGroupTextarea({ className, ...props }: React.ComponentProps<'textarea'>) {
|
|
141
|
+
return (
|
|
142
|
+
<Textarea
|
|
143
|
+
data-slot="input-group-control"
|
|
144
|
+
className={cn(
|
|
145
|
+
'flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent',
|
|
146
|
+
className
|
|
147
|
+
)}
|
|
148
|
+
{...props}
|
|
149
|
+
/>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export {
|
|
154
|
+
InputGroup,
|
|
155
|
+
InputGroupAddon,
|
|
156
|
+
InputGroupButton,
|
|
157
|
+
InputGroupText,
|
|
158
|
+
InputGroupInput,
|
|
159
|
+
InputGroupTextarea
|
|
160
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { cn } from '@cms/utils/cn'
|
|
4
|
+
import { OTPInput, OTPInputContext } from 'input-otp'
|
|
5
|
+
import { Minus } from 'lucide-react'
|
|
6
|
+
import * as React from 'react'
|
|
7
|
+
|
|
8
|
+
const InputOTP = React.forwardRef<
|
|
9
|
+
React.ElementRef<typeof OTPInput>,
|
|
10
|
+
React.ComponentPropsWithoutRef<typeof OTPInput>
|
|
11
|
+
>(({ className, containerClassName, ...props }, ref) => (
|
|
12
|
+
<OTPInput
|
|
13
|
+
ref={ref}
|
|
14
|
+
containerClassName={cn(
|
|
15
|
+
'flex items-center gap-2 has-[:disabled]:opacity-50',
|
|
16
|
+
containerClassName
|
|
17
|
+
)}
|
|
18
|
+
className={cn('disabled:cursor-not-allowed', className)}
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
))
|
|
22
|
+
InputOTP.displayName = 'InputOTP'
|
|
23
|
+
|
|
24
|
+
const InputOTPGroup = React.forwardRef<
|
|
25
|
+
React.ElementRef<'div'>,
|
|
26
|
+
React.ComponentPropsWithoutRef<'div'>
|
|
27
|
+
>(({ className, ...props }, ref) => (
|
|
28
|
+
<div ref={ref} className={cn('flex items-center', className)} {...props} />
|
|
29
|
+
))
|
|
30
|
+
InputOTPGroup.displayName = 'InputOTPGroup'
|
|
31
|
+
|
|
32
|
+
const InputOTPSlot = React.forwardRef<
|
|
33
|
+
React.ElementRef<'div'>,
|
|
34
|
+
React.ComponentPropsWithoutRef<'div'> & { index: number }
|
|
35
|
+
>(({ index, className, ...props }, ref) => {
|
|
36
|
+
const inputOTPContext = React.useContext(OTPInputContext)
|
|
37
|
+
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div
|
|
41
|
+
ref={ref}
|
|
42
|
+
className={cn(
|
|
43
|
+
'relative flex h-9 w-9 items-center justify-center border-y border-r border-input-border text-sm shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md',
|
|
44
|
+
isActive && 'z-10 ring-1 ring-ring',
|
|
45
|
+
className
|
|
46
|
+
)}
|
|
47
|
+
{...props}
|
|
48
|
+
>
|
|
49
|
+
{char}
|
|
50
|
+
{hasFakeCaret && (
|
|
51
|
+
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
|
52
|
+
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
|
|
53
|
+
</div>
|
|
54
|
+
)}
|
|
55
|
+
</div>
|
|
56
|
+
)
|
|
57
|
+
})
|
|
58
|
+
InputOTPSlot.displayName = 'InputOTPSlot'
|
|
59
|
+
|
|
60
|
+
const InputOTPSeparator = React.forwardRef<
|
|
61
|
+
React.ElementRef<'div'>,
|
|
62
|
+
React.ComponentPropsWithoutRef<'div'>
|
|
63
|
+
>(({ ...props }, ref) => (
|
|
64
|
+
<div ref={ref} role="presentation" aria-hidden="true" {...props}>
|
|
65
|
+
<Minus />
|
|
66
|
+
</div>
|
|
67
|
+
))
|
|
68
|
+
InputOTPSeparator.displayName = 'InputOTPSeparator'
|
|
69
|
+
|
|
70
|
+
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { cn } from '@cms/utils/cn'
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
|
|
4
|
+
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
|
|
5
|
+
({ className, type, ...props }, ref) => {
|
|
6
|
+
return (
|
|
7
|
+
<input
|
|
8
|
+
type={type}
|
|
9
|
+
className={cn(
|
|
10
|
+
'flex h-9 w-full rounded-md border border-input-border-border bg-input px-3 py-1 text-base transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
|
11
|
+
className
|
|
12
|
+
)}
|
|
13
|
+
ref={ref}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
)
|
|
19
|
+
Input.displayName = 'Input'
|
|
20
|
+
|
|
21
|
+
export { Input }
|