@polymarbot/nuxt-layer-shadcn-ui 0.9.5 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/app/components/ui/AdminLayout/types.ts +3 -2
  2. package/app/components/ui/Alert/index.vue +1 -5
  3. package/app/components/ui/Breadcrumb/types.ts +3 -1
  4. package/app/components/ui/Button/types.ts +2 -1
  5. package/app/components/ui/DataTable/index.vue +1 -1
  6. package/app/components/ui/Dropdown/ItemIcon.vue +1 -7
  7. package/app/components/ui/Icon/index.stories.ts +25 -0
  8. package/app/components/ui/Icon/index.vue +1 -0
  9. package/app/components/ui/Icon/types.ts +4 -2
  10. package/app/components/ui/ModalContent/types.ts +2 -2
  11. package/app/components/ui/Tabs/index.vue +2 -6
  12. package/app/components/ui/Upload/en.json +21 -0
  13. package/app/components/ui/Upload/index.stories.ts +381 -0
  14. package/app/components/ui/Upload/index.vue +598 -0
  15. package/app/components/ui/Upload/types.ts +36 -0
  16. package/i18n/messages/ar.json +21 -0
  17. package/i18n/messages/de.json +21 -0
  18. package/i18n/messages/en.json +21 -0
  19. package/i18n/messages/es.json +21 -0
  20. package/i18n/messages/fr.json +21 -0
  21. package/i18n/messages/hi.json +21 -0
  22. package/i18n/messages/id.json +21 -0
  23. package/i18n/messages/it.json +21 -0
  24. package/i18n/messages/ja.json +21 -0
  25. package/i18n/messages/ko.json +21 -0
  26. package/i18n/messages/nl.json +21 -0
  27. package/i18n/messages/pl.json +21 -0
  28. package/i18n/messages/pt.json +21 -0
  29. package/i18n/messages/ru.json +21 -0
  30. package/i18n/messages/th.json +21 -0
  31. package/i18n/messages/tr.json +21 -0
  32. package/i18n/messages/vi.json +21 -0
  33. package/i18n/messages/zh-CN.json +21 -0
  34. package/i18n/messages/zh-TW.json +21 -0
  35. package/package.json +2 -2
@@ -0,0 +1,598 @@
1
+ <script setup lang="ts">
2
+ import type { UploadFile, UploadProps } from './types'
3
+
4
+ const props = withDefaults(defineProps<UploadProps>(), {
5
+ variant: 'button',
6
+ accept: undefined,
7
+ beforeUpload: undefined,
8
+ upload: undefined,
9
+ disabled: false,
10
+ readonly: false,
11
+ invalid: false,
12
+ fileList: undefined,
13
+ multiple: false,
14
+ maxCount: undefined,
15
+ maxSize: undefined,
16
+ text: undefined,
17
+ icon: undefined,
18
+ directory: false,
19
+ class: undefined,
20
+ })
21
+
22
+ const emit = defineEmits<{
23
+ 'update:fileList': [files: UploadFile[]]
24
+ 'change': [files: UploadFile[]]
25
+ 'remove': [file: UploadFile]
26
+ 'preview': [file: UploadFile]
27
+ 'error': [error: unknown]
28
+ }>()
29
+
30
+ const T = useTranslations('components.ui.Upload')
31
+ const isInvalid = useFormItemInvalid(() => props.invalid)
32
+
33
+ const inputRef = ref<HTMLInputElement | null>(null)
34
+ const isDragOver = ref(false)
35
+ const uploadingFiles = ref<UploadFile[]>([])
36
+ const imageLoadErrors = ref<Set<string | number>>(new Set())
37
+ const internalFileList = ref<UploadFile[]>([])
38
+
39
+ const previewVisible = ref(false)
40
+ const previewFileItem = ref<UploadFile | null>(null)
41
+
42
+ const objectUrls = new Set<string>()
43
+ function trackObjectUrl (url: string | undefined) {
44
+ if (url) objectUrls.add(url)
45
+ }
46
+ onBeforeUnmount(() => {
47
+ objectUrls.forEach(url => URL.revokeObjectURL(url))
48
+ objectUrls.clear()
49
+ })
50
+
51
+ function nextUid () {
52
+ return `upload-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
53
+ }
54
+
55
+ const isControlled = computed(() => props.fileList !== undefined)
56
+
57
+ const effectiveFileList = computed<UploadFile[]>(() =>
58
+ isControlled.value ? (props.fileList ?? []) : internalFileList.value,
59
+ )
60
+
61
+ const displayList = computed<UploadFile[]>(() => [
62
+ ...effectiveFileList.value,
63
+ ...uploadingFiles.value,
64
+ ])
65
+
66
+ const reachedMax = computed(() =>
67
+ !!props.maxCount && displayList.value.length >= props.maxCount,
68
+ )
69
+
70
+ const canPickMore = computed(() =>
71
+ !props.disabled && !props.readonly && !reachedMax.value,
72
+ )
73
+
74
+ const canRemove = computed(() => !props.disabled && !props.readonly)
75
+
76
+ const showTrigger = computed(() => !props.readonly)
77
+
78
+ const emptyStateClass = 'py-2 text-xs text-muted-foreground'
79
+
80
+ const overlayIconButtonClass = `
81
+ text-white/80
82
+ hover:text-white
83
+ cursor-pointer transition
84
+ hover:scale-110
85
+ `
86
+
87
+ function formatBytes (bytes: number) {
88
+ if (bytes < 1024) return `${bytes}B`
89
+ if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)}KB`
90
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)}MB`
91
+ return `${(bytes / 1024 / 1024 / 1024).toFixed(1)}GB`
92
+ }
93
+
94
+ const maxSizeLabel = computed(() => props.maxSize ? formatBytes(props.maxSize) : '')
95
+
96
+ const allowMany = computed(() => props.multiple || props.directory)
97
+
98
+ const hintLines = computed(() => {
99
+ const lines: string[] = []
100
+ if (!allowMany.value) lines.push(T('hint.single'))
101
+ else if (props.maxCount) lines.push(T('hint.max', { max: props.maxCount }))
102
+ else lines.push(T('hint.multiple'))
103
+ if (maxSizeLabel.value) lines.push(T('hint.maxSize', { size: maxSizeLabel.value }))
104
+ return lines
105
+ })
106
+
107
+ const dragTitleText = computed(() => {
108
+ if (props.text) return props.text
109
+ return allowMany.value ? T('drag.titleMultiple') : T('drag.title')
110
+ })
111
+
112
+ const triggerLabel = computed(() => {
113
+ if (props.text) return props.text
114
+ return props.directory ? T('uploadDirectory') : T('upload')
115
+ })
116
+
117
+ const iconName = computed(() => {
118
+ if (props.icon) return props.icon
119
+ if (props.variant === 'box') return 'plus'
120
+ if (props.variant === 'drag') return 'inbox'
121
+ return 'upload'
122
+ })
123
+
124
+ const rootClass = computed(() => cn('w-full', props.class))
125
+
126
+ const dragAreaBase = `
127
+ group/upload
128
+ flex cursor-pointer flex-col items-center justify-center gap-2
129
+ rounded-md border border-dashed bg-muted/40 px-6 py-10
130
+ text-center transition-colors
131
+ dark:bg-muted/20
132
+ `
133
+
134
+ const boxTriggerBase = `
135
+ flex size-24 shrink-0 cursor-pointer flex-col items-center justify-center
136
+ gap-1 rounded-md border border-dashed bg-muted/40 text-xs
137
+ text-muted-foreground transition-colors
138
+ dark:bg-muted/20
139
+ `
140
+
141
+ const disabledClass = 'pointer-events-none cursor-not-allowed opacity-60'
142
+
143
+ function makeTriggerClass (base: string, idleHover: string) {
144
+ return cn(
145
+ base,
146
+ isInvalid.value ? 'border-danger' : idleHover,
147
+ isDragOver.value && !isInvalid.value && 'border-primary bg-primary/5',
148
+ props.disabled && disabledClass,
149
+ )
150
+ }
151
+
152
+ const dragAreaClass = computed(() => makeTriggerClass(
153
+ dragAreaBase,
154
+ `
155
+ border-border
156
+ hover:border-primary
157
+ `,
158
+ ))
159
+
160
+ const boxTriggerClass = computed(() => makeTriggerClass(
161
+ boxTriggerBase,
162
+ `
163
+ border-border
164
+ hover:border-primary hover:text-primary
165
+ `,
166
+ ))
167
+
168
+ const thumbBase = `
169
+ group/thumb
170
+ relative size-24 shrink-0 overflow-hidden rounded-md border bg-muted/40
171
+ dark:bg-muted/20
172
+ `
173
+
174
+ const rowItemBase = `
175
+ group/row
176
+ flex items-center gap-2 rounded-sm px-1 py-1 text-sm
177
+ hover:bg-muted/60
178
+ `
179
+
180
+ function thumbClass (file: UploadFile) {
181
+ const isError = file.status === 'error' || isInvalid.value
182
+ return cn(thumbBase, isError ? 'border-danger' : 'border-border')
183
+ }
184
+
185
+ function rowItemClass (file: UploadFile) {
186
+ const isError = file.status === 'error' || isInvalid.value
187
+ return cn(rowItemBase, isError && 'text-danger')
188
+ }
189
+
190
+ function isImageFile (file: UploadFile) {
191
+ if (file.uid != null && imageLoadErrors.value.has(file.uid)) return false
192
+ if (file.raw && file.raw.type) return file.raw.type.startsWith('image/')
193
+ return !!file.url
194
+ }
195
+
196
+ function onImageError (file: UploadFile) {
197
+ if (file.uid != null) imageLoadErrors.value.add(file.uid)
198
+ }
199
+
200
+ async function processFiles (files: File[]) {
201
+ if (!files.length) return
202
+
203
+ let candidates = files
204
+ if (!allowMany.value && candidates.length > 1) candidates = [ candidates[0]! ]
205
+
206
+ if (props.maxCount) {
207
+ const remaining = props.maxCount - displayList.value.length
208
+ if (remaining <= 0) return
209
+ candidates = candidates.slice(0, remaining)
210
+ }
211
+
212
+ if (props.maxSize) {
213
+ const limit = props.maxSize
214
+ const oversize = candidates.filter(f => f.size > limit)
215
+ if (oversize.length) {
216
+ const names = oversize.map(f => f.name).join(', ')
217
+ emit('error', new Error(T('error.oversize', { files: names, size: formatBytes(limit) })))
218
+ candidates = candidates.filter(f => f.size <= limit)
219
+ }
220
+ }
221
+
222
+ const accepted: (File | Blob)[] = []
223
+ for (const file of candidates) {
224
+ if (!props.beforeUpload) {
225
+ accepted.push(file)
226
+ continue
227
+ }
228
+ try {
229
+ const result = await props.beforeUpload(file)
230
+ if (result === false) continue
231
+ if (result instanceof File || result instanceof Blob) {
232
+ accepted.push(result)
233
+ } else {
234
+ accepted.push(file)
235
+ }
236
+ } catch {
237
+ // beforeUpload rejected — skip this file
238
+ }
239
+ }
240
+
241
+ if (!accepted.length) return
242
+
243
+ const entries: UploadFile[] = accepted.map((raw, i) => {
244
+ const isFile = raw instanceof File
245
+ const name = isFile ? raw.name : (candidates[i]?.name ?? 'file')
246
+ const url = raw.type.startsWith('image/') ? URL.createObjectURL(raw) : undefined
247
+ trackObjectUrl(url)
248
+ return {
249
+ uid: nextUid(),
250
+ name,
251
+ url,
252
+ status: 'uploading',
253
+ raw,
254
+ }
255
+ })
256
+
257
+ uploadingFiles.value.push(...entries)
258
+
259
+ if (!props.upload) {
260
+ finishUpload(entries, true)
261
+ return
262
+ }
263
+
264
+ try {
265
+ await props.upload(accepted)
266
+ finishUpload(entries, true)
267
+ } catch (err) {
268
+ finishUpload(entries, false)
269
+ emit('error', err)
270
+ }
271
+ }
272
+
273
+ function finishUpload (entries: UploadFile[], ok: boolean) {
274
+ const entryUids = new Set(entries.map(e => e.uid))
275
+ uploadingFiles.value = uploadingFiles.value.filter(e => !entryUids.has(e.uid))
276
+
277
+ if (!ok) {
278
+ uploadingFiles.value.push(...entries.map(e => ({ ...e, status: 'error' as const })))
279
+ return
280
+ }
281
+
282
+ const done = entries.map(e => ({ ...e, status: 'done' as const }))
283
+ const next = [ ...effectiveFileList.value, ...done ]
284
+ if (!isControlled.value) internalFileList.value = next
285
+ emit('update:fileList', next)
286
+ emit('change', next)
287
+ }
288
+
289
+ function onInputChange (e: Event) {
290
+ const target = e.target as HTMLInputElement
291
+ if (target.files?.length) processFiles(Array.from(target.files))
292
+ target.value = ''
293
+ }
294
+
295
+ function triggerSelect () {
296
+ if (!canPickMore.value) return
297
+ inputRef.value?.click()
298
+ }
299
+
300
+ function removeFile (file: UploadFile) {
301
+ const inUploading = uploadingFiles.value.some(f => f.uid === file.uid)
302
+ if (inUploading) {
303
+ uploadingFiles.value = uploadingFiles.value.filter(f => f.uid !== file.uid)
304
+ } else {
305
+ const next = effectiveFileList.value.filter(f => f.uid !== file.uid)
306
+ if (!isControlled.value) internalFileList.value = next
307
+ emit('update:fileList', next)
308
+ emit('change', next)
309
+ }
310
+ emit('remove', file)
311
+ }
312
+
313
+ function previewFile (file: UploadFile) {
314
+ emit('preview', file)
315
+ if (isImageFile(file) && file.url) {
316
+ previewFileItem.value = file
317
+ previewVisible.value = true
318
+ }
319
+ }
320
+
321
+ function onDragOver (e: DragEvent) {
322
+ e.preventDefault()
323
+ if (canPickMore.value) isDragOver.value = true
324
+ }
325
+ function onDragLeave (e: DragEvent) {
326
+ e.preventDefault()
327
+ isDragOver.value = false
328
+ }
329
+ function onDrop (e: DragEvent) {
330
+ e.preventDefault()
331
+ isDragOver.value = false
332
+ if (!canPickMore.value) return
333
+ const files = e.dataTransfer?.files
334
+ if (files?.length) processFiles(Array.from(files))
335
+ }
336
+ </script>
337
+
338
+ <template>
339
+ <div :class="rootClass">
340
+ <!-- Hidden native input -->
341
+ <input
342
+ ref="inputRef"
343
+ type="file"
344
+ class="hidden"
345
+ :accept="accept"
346
+ :multiple="multiple"
347
+ :disabled="disabled"
348
+ :webkitdirectory="directory || undefined"
349
+ @change="onInputChange"
350
+ >
351
+
352
+ <!-- button variant: button trigger -->
353
+ <div
354
+ v-if="variant === 'button' && showTrigger"
355
+ class="gap-3 flex flex-wrap items-center"
356
+ >
357
+ <Button
358
+ variant="outline"
359
+ :disabled="disabled || reachedMax"
360
+ @click="triggerSelect"
361
+ >
362
+ <Icon :name="iconName" />
363
+ {{ triggerLabel }}
364
+ </Button>
365
+ <div class="text-xs text-muted-foreground py-1.5">
366
+ <div
367
+ v-for="line in hintLines"
368
+ :key="line"
369
+ >
370
+ {{ line }}
371
+ </div>
372
+ </div>
373
+ </div>
374
+
375
+ <!-- drag variant: dashed drop area trigger -->
376
+ <div
377
+ v-else-if="variant === 'drag' && showTrigger"
378
+ :class="dragAreaClass"
379
+ role="button"
380
+ :aria-disabled="disabled || undefined"
381
+ @click="triggerSelect"
382
+ @dragover="onDragOver"
383
+ @dragleave="onDragLeave"
384
+ @drop="onDrop"
385
+ >
386
+ <Icon
387
+ :name="iconName"
388
+ class="size-10 text-primary"
389
+ :class="isInvalid && 'text-danger'"
390
+ />
391
+ <div class="text-foreground font-medium">
392
+ {{ dragTitleText }}
393
+ </div>
394
+ <div class="text-xs text-muted-foreground">
395
+ <div
396
+ v-for="line in hintLines"
397
+ :key="line"
398
+ >
399
+ {{ line }}
400
+ </div>
401
+ </div>
402
+ </div>
403
+
404
+ <!-- Shared row list (button + drag) -->
405
+ <template v-if="variant !== 'box'">
406
+ <ul
407
+ v-if="displayList.length"
408
+ :class="[ showTrigger && 'mt-2', 'flex flex-col' ]"
409
+ >
410
+ <li
411
+ v-for="file in displayList"
412
+ :key="file.uid"
413
+ :class="rowItemClass(file)"
414
+ >
415
+ <Icon
416
+ name="paperclip"
417
+ class="size-4 shrink-0"
418
+ />
419
+ <span class="min-w-0 flex-1 truncate">
420
+ {{ file.name }}
421
+ </span>
422
+ <Icon
423
+ v-if="file.status === 'uploading'"
424
+ name="loader-circle"
425
+ class="size-4 animate-spin text-muted-foreground shrink-0"
426
+ />
427
+ <button
428
+ v-if="canRemove"
429
+ type="button"
430
+ class="
431
+ text-muted-foreground
432
+ hover:text-foreground
433
+ shrink-0 cursor-pointer opacity-0 transition
434
+ group-hover/row:opacity-100
435
+ "
436
+ :aria-label="T('remove')"
437
+ @click="removeFile(file)"
438
+ >
439
+ <Icon name="trash-2" />
440
+ </button>
441
+ </li>
442
+ </ul>
443
+ <div
444
+ v-else-if="!showTrigger"
445
+ :class="emptyStateClass"
446
+ >
447
+ {{ T('empty') }}
448
+ </div>
449
+ </template>
450
+
451
+ <!-- box variant: grid of thumbs + add-more box -->
452
+ <template v-else>
453
+ <div
454
+ v-if="displayList.length || showTrigger"
455
+ class="gap-3 flex flex-wrap items-start"
456
+ >
457
+ <div
458
+ v-for="file in displayList"
459
+ :key="file.uid"
460
+ :class="thumbClass(file)"
461
+ >
462
+ <img
463
+ v-if="isImageFile(file) && file.url"
464
+ :src="file.url"
465
+ :alt="file.name"
466
+ class="size-full object-cover"
467
+ @error="onImageError(file)"
468
+ >
469
+ <div
470
+ v-else
471
+ class="
472
+ gap-1 p-2 flex size-full flex-col items-center justify-center
473
+ "
474
+ >
475
+ <Icon
476
+ name="image"
477
+ class="size-6 text-muted-foreground"
478
+ :class="file.status === 'error' && 'text-danger'"
479
+ />
480
+ <span
481
+ class="px-1 leading-tight line-clamp-2 text-center text-[10px]"
482
+ >
483
+ {{ file.name }}
484
+ </span>
485
+ </div>
486
+
487
+ <!-- Uploading overlay -->
488
+ <div
489
+ v-if="file.status === 'uploading'"
490
+ class="
491
+ inset-0 gap-2 bg-background/80 px-3 text-xs absolute flex flex-col
492
+ items-center justify-center
493
+ "
494
+ >
495
+ <span>{{ T('uploading') }}</span>
496
+ <div class="h-1 bg-muted w-full overflow-hidden rounded-full">
497
+ <div class="upload-progress bg-primary h-full" />
498
+ </div>
499
+ </div>
500
+
501
+ <!-- Hover overlay -->
502
+ <div
503
+ v-else-if="isImageFile(file) || canRemove"
504
+ class="
505
+ inset-0 gap-3 bg-black/50 absolute flex items-center
506
+ justify-center opacity-0 transition
507
+ group-hover/thumb:opacity-100
508
+ "
509
+ >
510
+ <button
511
+ v-if="isImageFile(file) && file.url"
512
+ type="button"
513
+ :class="overlayIconButtonClass"
514
+ :aria-label="T('preview')"
515
+ @click="previewFile(file)"
516
+ >
517
+ <Icon name="eye" />
518
+ </button>
519
+ <button
520
+ v-if="canRemove"
521
+ type="button"
522
+ :class="overlayIconButtonClass"
523
+ :aria-label="T('remove')"
524
+ @click="removeFile(file)"
525
+ >
526
+ <Icon name="trash-2" />
527
+ </button>
528
+ </div>
529
+ </div>
530
+
531
+ <button
532
+ v-if="showTrigger && !reachedMax"
533
+ type="button"
534
+ :class="boxTriggerClass"
535
+ :disabled="disabled"
536
+ @click="triggerSelect"
537
+ @dragover="onDragOver"
538
+ @dragleave="onDragLeave"
539
+ @drop="onDrop"
540
+ >
541
+ <Icon
542
+ :name="iconName"
543
+ class="size-5"
544
+ />
545
+ <span>{{ triggerLabel }}</span>
546
+ </button>
547
+ </div>
548
+
549
+ <div
550
+ v-else
551
+ :class="emptyStateClass"
552
+ >
553
+ {{ T('empty') }}
554
+ </div>
555
+
556
+ <div
557
+ v-if="showTrigger"
558
+ class="mt-2 text-xs text-muted-foreground"
559
+ >
560
+ <div
561
+ v-for="line in hintLines"
562
+ :key="line"
563
+ >
564
+ {{ line }}
565
+ </div>
566
+ </div>
567
+ </template>
568
+
569
+ <!-- Image preview modal (box variant only) -->
570
+ <Modal
571
+ v-if="variant === 'box'"
572
+ v-model:visible="previewVisible"
573
+ hideHeader
574
+ hideFooter
575
+ :title="previewFileItem?.name"
576
+ class="max-w-3xl"
577
+ >
578
+ <img
579
+ v-if="previewFileItem?.url"
580
+ :src="previewFileItem.url"
581
+ :alt="previewFileItem.name"
582
+ class="mx-auto max-h-[70vh] max-w-full object-contain"
583
+ >
584
+ </Modal>
585
+ </div>
586
+ </template>
587
+
588
+ <style scoped>
589
+ @keyframes upload-progress-anim {
590
+ 0% { transform: translateX(-100%); }
591
+ 100% { transform: translateX(100%); }
592
+ }
593
+
594
+ .upload-progress {
595
+ width: 60%;
596
+ animation: upload-progress-anim 1.2s ease-in-out infinite;
597
+ }
598
+ </style>
@@ -0,0 +1,36 @@
1
+ import type { Component } from 'vue'
2
+
3
+ export type UploadVariant = 'button' | 'box' | 'drag'
4
+
5
+ export type UploadFileStatus = 'uploading' | 'done' | 'error'
6
+
7
+ export interface UploadFile {
8
+ uid?: string | number
9
+ name: string
10
+ url?: string
11
+ status?: UploadFileStatus
12
+ raw?: File | Blob
13
+ }
14
+
15
+ export type UploadBeforeUpload
16
+ = (file: File) => boolean | undefined | Promise<File | Blob | boolean | undefined>
17
+
18
+ export type UploadHandler = (files: (File | Blob)[]) => Promise<unknown>
19
+
20
+ export interface UploadProps {
21
+ variant?: UploadVariant
22
+ accept?: string
23
+ beforeUpload?: UploadBeforeUpload
24
+ upload?: UploadHandler
25
+ disabled?: boolean
26
+ readonly?: boolean
27
+ invalid?: boolean
28
+ fileList?: UploadFile[]
29
+ multiple?: boolean
30
+ maxCount?: number
31
+ maxSize?: number
32
+ text?: string
33
+ icon?: string | Component
34
+ directory?: boolean
35
+ class?: ClassValue
36
+ }
@@ -77,6 +77,27 @@
77
77
  "noItems": "لا توجد خيارات",
78
78
  "placeholder": "يرجى اختيار",
79
79
  "searchPlaceholder": "البحث..."
80
+ },
81
+ "Upload": {
82
+ "drag": {
83
+ "title": "انقر أو اسحب الملف إلى هذه المنطقة للتحميل",
84
+ "titleMultiple": "انقر أو اسحب الملفات إلى هذه المنطقة للتحميل"
85
+ },
86
+ "empty": "لا توجد ملفات",
87
+ "error": {
88
+ "oversize": "{files} يتجاوز حد الحجم {size}."
89
+ },
90
+ "hint": {
91
+ "max": "يمكن تحديد {max} ملفات بحد أقصى.",
92
+ "maxSize": "الحد الأقصى للحجم لكل ملف: {size}.",
93
+ "multiple": "يمكن تحديد ملفات متعددة.",
94
+ "single": "يمكن تحديد ملف واحد فقط."
95
+ },
96
+ "preview": "معاينة",
97
+ "remove": "إزالة",
98
+ "upload": "تحميل",
99
+ "uploadDirectory": "تحميل الدليل",
100
+ "uploading": "جاري التحميل"
80
101
  }
81
102
  }
82
103
  }
@@ -77,6 +77,27 @@
77
77
  "noItems": "Keine Optionen",
78
78
  "placeholder": "Bitte wählen",
79
79
  "searchPlaceholder": "Suchen..."
80
+ },
81
+ "Upload": {
82
+ "drag": {
83
+ "title": "Klicken oder ziehen Sie eine Datei in diesen Bereich, um sie hochzuladen",
84
+ "titleMultiple": "Klicken oder ziehen Sie Dateien in diesen Bereich, um sie hochzuladen"
85
+ },
86
+ "empty": "Keine Dateien",
87
+ "error": {
88
+ "oversize": "{files} überschreitet das {size} Größenlimit."
89
+ },
90
+ "hint": {
91
+ "max": "Es können bis zu {max} Dateien ausgewählt werden.",
92
+ "maxSize": "Maximale Dateigröße: {size}.",
93
+ "multiple": "Es können mehrere Dateien ausgewählt werden.",
94
+ "single": "Es kann nur eine einzelne Datei ausgewählt werden."
95
+ },
96
+ "preview": "Vorschau",
97
+ "remove": "Entfernen",
98
+ "upload": "Hochladen",
99
+ "uploadDirectory": "Verzeichnis hochladen",
100
+ "uploading": "Wird hochgeladen"
80
101
  }
81
102
  }
82
103
  }