@maestro-js/components 1.0.0-alpha.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/commands/add.ts +41 -0
- package/commands/index.ts +7 -0
- package/commands/list.ts +9 -0
- package/dist/components.json +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +48 -0
- package/package.json +49 -0
- package/registry.json +1445 -0
- package/scripts/build.ts +44 -0
- package/src/components/alert-dialog.tsx +150 -0
- package/src/components/autocomplete.tsx +288 -0
- package/src/components/autosuggest.tsx +314 -0
- package/src/components/avatar.tsx +54 -0
- package/src/components/boolean-select.tsx +48 -0
- package/src/components/button-group.tsx +75 -0
- package/src/components/button-link.tsx +71 -0
- package/src/components/button.tsx +132 -0
- package/src/components/checkbox-group.tsx +172 -0
- package/src/components/checkbox.tsx +207 -0
- package/src/components/chip.tsx +158 -0
- package/src/components/container.tsx +82 -0
- package/src/components/currency-field.tsx +183 -0
- package/src/components/date-input.tsx +189 -0
- package/src/components/date-picker.tsx +211 -0
- package/src/components/date-range-picker.tsx +290 -0
- package/src/components/date-time-picker.tsx +196 -0
- package/src/components/dialog.tsx +97 -0
- package/src/components/disclosure.tsx +114 -0
- package/src/components/drawer.tsx +78 -0
- package/src/components/enum-chip.tsx +30 -0
- package/src/components/file-input.tsx +245 -0
- package/src/components/form.tsx +82 -0
- package/src/components/headless-file-input.tsx +362 -0
- package/src/components/helpers/animated-popover.tsx +24 -0
- package/src/components/helpers/button-context.ts +10 -0
- package/src/components/helpers/calendar-month-year-picker.tsx +280 -0
- package/src/components/helpers/form-field.tsx +229 -0
- package/src/components/helpers/get-button-classes.ts +138 -0
- package/src/components/helpers/headless-button.tsx +36 -0
- package/src/components/helpers/pdf-dist.client.ts +6 -0
- package/src/components/icon.tsx +26 -0
- package/src/components/image-input.tsx +265 -0
- package/src/components/img.tsx +46 -0
- package/src/components/inline-alert.tsx +54 -0
- package/src/components/labeled-value.tsx +480 -0
- package/src/components/link-tabs.tsx +118 -0
- package/src/components/menu.tsx +152 -0
- package/src/components/month-day-input.tsx +176 -0
- package/src/components/multi-file-input.tsx +244 -0
- package/src/components/multi-image-input.tsx +389 -0
- package/src/components/multicomplete.tsx +322 -0
- package/src/components/multiselect.tsx +325 -0
- package/src/components/multisuggest.tsx +357 -0
- package/src/components/number-field.tsx +143 -0
- package/src/components/numeric-tag-field.tsx +271 -0
- package/src/components/pdf-input.tsx +249 -0
- package/src/components/pdf.tsx +86 -0
- package/src/components/percentage-field.tsx +187 -0
- package/src/components/phone-number-field.tsx +166 -0
- package/src/components/radio-group.tsx +112 -0
- package/src/components/radio.tsx +91 -0
- package/src/components/select.tsx +215 -0
- package/src/components/spinner.tsx +16 -0
- package/src/components/stepper.tsx +181 -0
- package/src/components/switch.tsx +186 -0
- package/src/components/tabs.tsx +151 -0
- package/src/components/tag-field.tsx +250 -0
- package/src/components/text-area.tsx +148 -0
- package/src/components/text-field.tsx +144 -0
- package/src/components/time-input.tsx +198 -0
- package/src/components/toast.tsx +176 -0
- package/src/components/toggle-button-group.tsx +94 -0
- package/src/components/year-month-input.tsx +187 -0
- package/src/utils/colors.ts +44 -0
- package/src/utils/compose-refs.ts +32 -0
- package/src/utils/enum-colors.ts +1 -0
- package/src/utils/file-input.ts +49 -0
- package/src/utils/icons.d.ts +20 -0
- package/src/utils/tw.ts +13 -0
- package/src/utils/use-element-size.ts +35 -0
- package/src/utils/use-number-input.ts +143 -0
- package/src/utils/use-pagination.ts +38 -0
- package/src/utils/use-prevent-default.ts +27 -0
- package/src/utils/use-render-props.ts +39 -0
- package/src/utils/use-spin-delay.ts +24 -0
- package/src/utils/use-stable-accessor.ts +11 -0
- package/src/utils/use-tab-indicator.ts +106 -0
- package/tests/commands.test.ts +81 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { HeadlessForm } from '@maestro-js/form'
|
|
3
|
+
import type { ValidationFunction, ValidationResult } from '@maestro-js/form'
|
|
4
|
+
import { tw } from '../utils/tw'
|
|
5
|
+
import { formatFileSize, checkFileType, checkFileMaxSize } from '../utils/file-input'
|
|
6
|
+
import { useStableAccessor } from '../utils/use-stable-accessor'
|
|
7
|
+
import { FormFieldComponents } from './helpers/form-field'
|
|
8
|
+
import { HeadlessFileInput } from './headless-file-input'
|
|
9
|
+
import { Icon } from './icon'
|
|
10
|
+
|
|
11
|
+
export type MultiImageInputProps = {
|
|
12
|
+
// Form identification
|
|
13
|
+
name?: string
|
|
14
|
+
form?: string
|
|
15
|
+
|
|
16
|
+
// Display elements
|
|
17
|
+
label?: React.ReactNode
|
|
18
|
+
cornerHint?: React.ReactNode
|
|
19
|
+
helpText?: React.ReactNode
|
|
20
|
+
warningMessage?: React.ReactNode
|
|
21
|
+
placeholder?: string
|
|
22
|
+
|
|
23
|
+
// Validation and state
|
|
24
|
+
isRequired?: boolean
|
|
25
|
+
isDisabled?: boolean
|
|
26
|
+
isInvalid?: boolean
|
|
27
|
+
validate?: ValidationFunction<(string | File)[]>
|
|
28
|
+
errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)
|
|
29
|
+
|
|
30
|
+
// Value management (File objects array or strings, held in memory until form submit)
|
|
31
|
+
value?: (string | File)[]
|
|
32
|
+
defaultValue?: (string | File)[]
|
|
33
|
+
onChange?: (value: (string | File)[]) => void
|
|
34
|
+
|
|
35
|
+
// Image specific
|
|
36
|
+
accept?: string
|
|
37
|
+
maxSize?: number // in bytes per image
|
|
38
|
+
maxImages?: number
|
|
39
|
+
aspectRatio?: number // width/height ratio
|
|
40
|
+
columns?: number // Grid columns for display
|
|
41
|
+
allowReordering?: boolean
|
|
42
|
+
|
|
43
|
+
renderPreview?: (props: MultiImageInputPreviewProps) => React.ReactNode
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface MultiImageInputPreviewProps {
|
|
47
|
+
fileUrl: string
|
|
48
|
+
isDisabled: boolean
|
|
49
|
+
index: number
|
|
50
|
+
onChange(value: string | null | File): unknown
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function MultiImageInput(props: MultiImageInputProps) {
|
|
54
|
+
const inputRef = React.useRef<HTMLInputElement>(null)
|
|
55
|
+
const helpTextId = React.useId()
|
|
56
|
+
const inputId = React.useId()
|
|
57
|
+
const labelRef = React.useRef<HTMLLabelElement>(null)
|
|
58
|
+
|
|
59
|
+
const exceededMaxSizeFn = React.useCallback(
|
|
60
|
+
(value: (string | File)[]) => {
|
|
61
|
+
for (let i = 0; i < value.length; i++) {
|
|
62
|
+
const v = value[i]
|
|
63
|
+
if (v instanceof File && props.maxSize) {
|
|
64
|
+
const error = checkFileMaxSize(v, props.maxSize)
|
|
65
|
+
if (error) return `Image ${i + 1} exceeded max size of ${formatFileSize(props.maxSize)}`
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null
|
|
69
|
+
},
|
|
70
|
+
[props.maxSize]
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
const formatNotSupportedFn = React.useCallback(
|
|
74
|
+
(value: (string | File)[]) => {
|
|
75
|
+
const accept = props.accept ?? 'image/*'
|
|
76
|
+
for (const v of value) {
|
|
77
|
+
if (v instanceof File) {
|
|
78
|
+
const error = checkFileType(v, accept)
|
|
79
|
+
if (error) return error
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return null
|
|
83
|
+
},
|
|
84
|
+
[props.accept]
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
// Controlled state management
|
|
88
|
+
const [value, setValue] = HeadlessForm.useControlledState(props.value, props.defaultValue ?? [], (value) => {
|
|
89
|
+
if (props.onChange) {
|
|
90
|
+
props.onChange(value)
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// Form validation
|
|
95
|
+
const {
|
|
96
|
+
validationMessage,
|
|
97
|
+
isInvalid: validationInvalid,
|
|
98
|
+
commitValidation
|
|
99
|
+
} = HeadlessForm.useValidation(
|
|
100
|
+
{
|
|
101
|
+
validate(files) {
|
|
102
|
+
const exceededMaxSize = exceededMaxSizeFn(files)
|
|
103
|
+
const formatNotSupported = formatNotSupportedFn(files)
|
|
104
|
+
const exceededMaxImages = props.maxImages !== undefined && files.length > props.maxImages ? 'Too many images' : null
|
|
105
|
+
const custom = [props.validate ? props.validate(files) : []].flat()
|
|
106
|
+
|
|
107
|
+
const errors = [exceededMaxSize, formatNotSupported, exceededMaxImages, ...custom].filter(Boolean)
|
|
108
|
+
return errors as string[]
|
|
109
|
+
},
|
|
110
|
+
value,
|
|
111
|
+
name: props.name,
|
|
112
|
+
isRequired: props.isRequired,
|
|
113
|
+
form: props.form,
|
|
114
|
+
focus() {
|
|
115
|
+
labelRef.current?.focus()
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
inputRef
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
const isInvalid = props.isInvalid || validationInvalid
|
|
122
|
+
const hasWarning = Boolean(props.warningMessage) && !isInvalid
|
|
123
|
+
const errorMessage = validationMessage
|
|
124
|
+
|
|
125
|
+
// Stable handlers via useStableAccessor to avoid re-creating on every render
|
|
126
|
+
const getValue = useStableAccessor(value)
|
|
127
|
+
const getSetValue = useStableAccessor(setValue)
|
|
128
|
+
|
|
129
|
+
const handleReorder = React.useCallback(
|
|
130
|
+
(fromIndex: number, toIndex: number) => {
|
|
131
|
+
const newFiles = [...getValue()]
|
|
132
|
+
const [movedFile] = newFiles.splice(fromIndex, 1)
|
|
133
|
+
newFiles.splice(toIndex, 0, movedFile!)
|
|
134
|
+
getSetValue()(newFiles)
|
|
135
|
+
},
|
|
136
|
+
[getValue, getSetValue]
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
const handleRemove = React.useCallback(
|
|
140
|
+
(index: number) => {
|
|
141
|
+
getSetValue()(getValue().filter((_: string | File, i: number) => i !== index))
|
|
142
|
+
},
|
|
143
|
+
[getValue, getSetValue]
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
const handleReplace = React.useCallback(
|
|
147
|
+
(index: number, file: string | File) => {
|
|
148
|
+
getSetValue()(getValue().map((f: string | File, i: number) => (i === index ? file : f)))
|
|
149
|
+
},
|
|
150
|
+
[getValue, getSetValue]
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
const getCommitValidation = useStableAccessor(commitValidation)
|
|
154
|
+
const handleFileInputChange = React.useCallback(
|
|
155
|
+
(v: (string | File)[]) => {
|
|
156
|
+
getSetValue()(v)
|
|
157
|
+
getCommitValidation()()
|
|
158
|
+
},
|
|
159
|
+
[getSetValue, getCommitValidation]
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
// Stable component reference
|
|
163
|
+
const imagePreview = React.useMemo(() => props.renderPreview ?? DefaultImagePreview, [props.renderPreview])
|
|
164
|
+
|
|
165
|
+
// Stable keys for grid items
|
|
166
|
+
const fileKeyMapRef = React.useRef(new WeakMap<File, string>())
|
|
167
|
+
const fileKeyCounterRef = React.useRef(0)
|
|
168
|
+
const getFileKey = React.useCallback((item: string | File): string => {
|
|
169
|
+
if (typeof item === 'string') return item
|
|
170
|
+
let key = fileKeyMapRef.current.get(item)
|
|
171
|
+
if (!key) {
|
|
172
|
+
key = `file-${fileKeyCounterRef.current++}`
|
|
173
|
+
fileKeyMapRef.current.set(item, key)
|
|
174
|
+
}
|
|
175
|
+
return key
|
|
176
|
+
}, [])
|
|
177
|
+
|
|
178
|
+
// Memoize grid style
|
|
179
|
+
const gridStyle = React.useMemo(
|
|
180
|
+
() => ({ gridTemplateColumns: `repeat(${props.columns ?? 4}, minmax(0, 1fr))` }),
|
|
181
|
+
[props.columns]
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<HeadlessFileInput value={value} onChange={handleFileInputChange} multiple>
|
|
186
|
+
<div className="relative">
|
|
187
|
+
<FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} htmlFor={inputId} />
|
|
188
|
+
|
|
189
|
+
<HeadlessFileInput.Preview>
|
|
190
|
+
{(files) => {
|
|
191
|
+
const canAddMore = !props.maxImages || files.length < props.maxImages
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<>
|
|
195
|
+
{/* Drop zone */}
|
|
196
|
+
{canAddMore && (
|
|
197
|
+
<HeadlessFileInput.DropZone
|
|
198
|
+
accept={props.accept ?? 'image/*'}
|
|
199
|
+
name={props.name}
|
|
200
|
+
inputId={inputId}
|
|
201
|
+
inputRef={inputRef}
|
|
202
|
+
ref={labelRef}
|
|
203
|
+
disabled={props.isDisabled}
|
|
204
|
+
>
|
|
205
|
+
{({ dragOver }) => (
|
|
206
|
+
<div
|
|
207
|
+
className={tw(
|
|
208
|
+
'relative flex flex-col items-center justify-center rounded-md border-2 border-dashed cursor-pointer transition-all duration-200 mb-2 p-8',
|
|
209
|
+
'focus:outline-none focus-within:ring-2 focus-within:ring-primary-500 focus-within:ring-offset-2 pointer-events-none',
|
|
210
|
+
'border-neutral-200 hover:border-neutral-300',
|
|
211
|
+
props.isDisabled && 'border-neutral-100 cursor-not-allowed',
|
|
212
|
+
hasWarning &&
|
|
213
|
+
'border-notice-300 hover:border-notice-400 text-notice-900 placeholder-notice-300 focus-within:border-notice-500 focus-within:ring-notice-500',
|
|
214
|
+
isInvalid &&
|
|
215
|
+
'border-negative-300 hover:border-negative-400 text-negative-900 placeholder-negative-300 focus-within:border-negative-500 focus-within:ring-negative-500'
|
|
216
|
+
)}
|
|
217
|
+
>
|
|
218
|
+
{dragOver ? (
|
|
219
|
+
<div className="min-h-20">
|
|
220
|
+
<DragOverContent />
|
|
221
|
+
</div>
|
|
222
|
+
) : (
|
|
223
|
+
<div className="text-center">
|
|
224
|
+
<Icon name="cloud-arrow-up" className="h-10 w-10 mx-auto mb-3 text-neutral-400" />
|
|
225
|
+
<p className="text-sm text-neutral-600 font-medium">
|
|
226
|
+
{props.placeholder || 'Drop images here or click to browse'}
|
|
227
|
+
</p>
|
|
228
|
+
{props.maxSize && (
|
|
229
|
+
<p className="text-xs text-neutral-500 mt-1">
|
|
230
|
+
Max size: {formatFileSize(props.maxSize)} per image
|
|
231
|
+
</p>
|
|
232
|
+
)}
|
|
233
|
+
{props.maxImages && (
|
|
234
|
+
<p className="text-xs text-neutral-500 mt-1">
|
|
235
|
+
{files.length} / {props.maxImages} images uploaded
|
|
236
|
+
</p>
|
|
237
|
+
)}
|
|
238
|
+
</div>
|
|
239
|
+
)}
|
|
240
|
+
</div>
|
|
241
|
+
)}
|
|
242
|
+
</HeadlessFileInput.DropZone>
|
|
243
|
+
)}
|
|
244
|
+
|
|
245
|
+
{/* Image previews grid */}
|
|
246
|
+
{files.length > 0 && (
|
|
247
|
+
<div className={tw('grid gap-3')} style={gridStyle}>
|
|
248
|
+
{files.map((fileItem, index) => (
|
|
249
|
+
<GridItem
|
|
250
|
+
key={getFileKey(value[index]!)}
|
|
251
|
+
fileUrl={fileItem.fileUrl}
|
|
252
|
+
index={index}
|
|
253
|
+
aspectRatio={props.aspectRatio ?? 1}
|
|
254
|
+
isDisabled={props.isDisabled ?? false}
|
|
255
|
+
allowReordering={props.allowReordering ?? false}
|
|
256
|
+
onReorder={handleReorder}
|
|
257
|
+
onRemove={handleRemove}
|
|
258
|
+
onReplace={handleReplace}
|
|
259
|
+
ImagePreview={imagePreview}
|
|
260
|
+
/>
|
|
261
|
+
))}
|
|
262
|
+
</div>
|
|
263
|
+
)}
|
|
264
|
+
|
|
265
|
+
{/* Message when at max capacity */}
|
|
266
|
+
{!canAddMore && (
|
|
267
|
+
<div className="mt-3 text-sm text-neutral-500 text-center">
|
|
268
|
+
Maximum of {props.maxImages} images reached
|
|
269
|
+
</div>
|
|
270
|
+
)}
|
|
271
|
+
</>
|
|
272
|
+
)
|
|
273
|
+
}}
|
|
274
|
+
</HeadlessFileInput.Preview>
|
|
275
|
+
|
|
276
|
+
<FormFieldComponents.FormFieldHelpText
|
|
277
|
+
id={helpTextId}
|
|
278
|
+
isInvalid={isInvalid}
|
|
279
|
+
validationMessage={errorMessage}
|
|
280
|
+
hasWarning={hasWarning}
|
|
281
|
+
warningMessage={props.warningMessage}
|
|
282
|
+
helpText={props.helpText}
|
|
283
|
+
/>
|
|
284
|
+
</div>
|
|
285
|
+
</HeadlessFileInput>
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ── Memoized sub-components ─────────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
const GridItem = React.memo(function GridItem({
|
|
292
|
+
fileUrl,
|
|
293
|
+
index,
|
|
294
|
+
aspectRatio,
|
|
295
|
+
isDisabled,
|
|
296
|
+
allowReordering,
|
|
297
|
+
onReorder,
|
|
298
|
+
onRemove,
|
|
299
|
+
onReplace,
|
|
300
|
+
ImagePreview
|
|
301
|
+
}: {
|
|
302
|
+
fileUrl: string
|
|
303
|
+
index: number
|
|
304
|
+
aspectRatio: number
|
|
305
|
+
isDisabled: boolean
|
|
306
|
+
allowReordering: boolean
|
|
307
|
+
onReorder: (fromIndex: number, toIndex: number) => void
|
|
308
|
+
onRemove: (index: number) => void
|
|
309
|
+
onReplace: (index: number, file: string | File) => void
|
|
310
|
+
ImagePreview: (props: MultiImageInputPreviewProps) => React.ReactNode
|
|
311
|
+
}) {
|
|
312
|
+
const handleDragStart = React.useCallback(
|
|
313
|
+
(e: React.DragEvent) => e.dataTransfer.setData('index', index.toString()),
|
|
314
|
+
[index]
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
const handleDrop = React.useCallback(
|
|
318
|
+
(e: React.DragEvent) => {
|
|
319
|
+
e.preventDefault()
|
|
320
|
+
const fromIndex = parseInt(e.dataTransfer.getData('index'))
|
|
321
|
+
if (fromIndex !== index) onReorder(fromIndex, index)
|
|
322
|
+
},
|
|
323
|
+
[index, onReorder]
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
const handleChange = React.useCallback(
|
|
327
|
+
(v: string | null | File) => {
|
|
328
|
+
if (v === null) onRemove(index)
|
|
329
|
+
else onReplace(index, v)
|
|
330
|
+
},
|
|
331
|
+
[index, onRemove, onReplace]
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
return (
|
|
335
|
+
<div
|
|
336
|
+
className="relative group overflow-hidden rounded-md"
|
|
337
|
+
style={{ aspectRatio }}
|
|
338
|
+
draggable={!isDisabled && allowReordering}
|
|
339
|
+
onDragStart={handleDragStart}
|
|
340
|
+
onDragOver={preventDefaultHandler}
|
|
341
|
+
onDrop={handleDrop}
|
|
342
|
+
>
|
|
343
|
+
<ImagePreview isDisabled={isDisabled} fileUrl={fileUrl} index={index} onChange={handleChange} />
|
|
344
|
+
</div>
|
|
345
|
+
)
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
const DefaultImagePreview = React.memo(function DefaultImagePreview({
|
|
349
|
+
fileUrl,
|
|
350
|
+
isDisabled,
|
|
351
|
+
onChange
|
|
352
|
+
}: MultiImageInputPreviewProps) {
|
|
353
|
+
return (
|
|
354
|
+
<>
|
|
355
|
+
<img src={fileUrl} alt="Preview" className="w-full h-full object-cover rounded-md" />
|
|
356
|
+
{!isDisabled && (
|
|
357
|
+
<button
|
|
358
|
+
type="button"
|
|
359
|
+
aria-label="Remove image"
|
|
360
|
+
onClick={() => onChange(null)}
|
|
361
|
+
className="absolute top-1 right-1 p-1 bg-black/50 hover:bg-red-600 rounded-full transition-all duration-200 hover:scale-110"
|
|
362
|
+
>
|
|
363
|
+
<Icon name="x-mark" className="h-4 w-4 text-white" />
|
|
364
|
+
</button>
|
|
365
|
+
)}
|
|
366
|
+
</>
|
|
367
|
+
)
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
// ── Static helpers ──────────────────────────────────────────────────────────
|
|
371
|
+
|
|
372
|
+
const DRAG_OVER_STYLE = {
|
|
373
|
+
background: 'repeating-linear-gradient(45deg, #e5e7eb, #e5e7eb 20px, #d1d5db 20px, #d1d5db 50px)'
|
|
374
|
+
} as const
|
|
375
|
+
|
|
376
|
+
function DragOverContent() {
|
|
377
|
+
return (
|
|
378
|
+
<div className="absolute inset-0 p-6 rounded-md flex items-center justify-center text-center" style={DRAG_OVER_STYLE}>
|
|
379
|
+
<div>
|
|
380
|
+
<Icon name="cloud-arrow-up" className="h-12 w-12 mx-auto text-gray-400" />
|
|
381
|
+
<span className="mt-2 block text-sm font-semibold text-gray-900">Drop Image</span>
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function preventDefaultHandler(e: React.DragEvent) {
|
|
388
|
+
e.preventDefault()
|
|
389
|
+
}
|