+
@@ -126,10 +98,14 @@ const files = ref([
-
- {{ files?.[0]?.name }}
-
-
-
+
+
+
diff --git a/src/runtime/components/FileUpload.vue b/src/runtime/components/FileUpload.vue
index 5b708ff1..ca491683 100644
--- a/src/runtime/components/FileUpload.vue
+++ b/src/runtime/components/FileUpload.vue
@@ -2,6 +2,8 @@
import type { AppConfig } from '@nuxt/schema'
import type { UseFileDialogReturn } from '@vueuse/core'
import theme from '#build/ui/file-upload'
+import type { UseComponentIconsProps } from '../composables/useComponentIcons'
+import type { ButtonProps } from '../types'
import type { ComponentConfig } from '../types/utils'
type FileUpload = ComponentConfig
@@ -14,6 +16,19 @@ export interface FileUploadProps {
as?: any
id?: string
name?: string
+ /**
+ * The icon to display.
+ * @defaultValue appConfig.ui.icons.upload
+ * @IconifyIcon
+ */
+ icon?: string
+ label?: string
+ description?: string
+ actions?: ButtonProps[]
+ /**
+ * @defaultValue 'md'
+ */
+ size?: FileUpload['variants']['size']
required?: boolean
disabled?: boolean
multiple?: M & boolean
@@ -30,7 +45,7 @@ export interface FileUploadProps {
reset?: boolean
/**
* Create a zone that allows the user to drop files onto it.
- * @defaultValue false
+ * @defaultValue true
*/
dropzone?: boolean
defaultValue?: File[]
@@ -47,8 +62,12 @@ export interface FileUploadSlots {
open: UseFileDialogReturn['open']
reset: UseFileDialogReturn['reset']
previewUrls: string[]
- files: FileList[]
}): any
+ leading(props?: {}): any
+ label(props?: {}): any
+ description(props?: {}): any
+ actions(props?: {}): any
+ preview(props?: {}): any
}
@@ -58,28 +77,24 @@ import { Primitive } from 'reka-ui'
import { useFileDialog, useDropZone } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { useFormField } from '../composables/useFormField'
-
import { tv } from '../utils/tv'
+import UButton from './Button.vue'
defineOptions({ inheritAttrs: false })
const props = withDefaults(defineProps>(), {
- accept: '*'
+ accept: '*',
+ dropzone: true
})
defineEmits>()
-defineSlots()
+const slots = defineSlots()
const modelValue = defineModel<(M extends true ? File[] : File) | null>()
const appConfig = useAppConfig() as FileUpload['AppConfig']
-const { emitFormInput, emitFormChange, id, name, disabled, ariaAttrs } = useFormField(props, { deferInputValidation: true })
-
-const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.fileUpload || {}) })({
- dropzone: props.dropzone
-}))
-
const inputRef = ref()
+const dropZoneRef = ref()
const { files, open, reset, onCancel, onChange } = useFileDialog({
multiple: props.multiple,
@@ -88,6 +103,18 @@ const { files, open, reset, onCancel, onChange } = useFileDialog({
input: inputRef.value,
initialFiles: props.defaultValue
})
+const { isOverDropZone } = useDropZone(dropZoneRef, {
+ onDrop,
+ dataTypes: props.accept.split(','),
+ multiple: props.multiple
+})
+const { emitFormInput, emitFormChange, id, name, disabled, ariaAttrs } = useFormField(props, { deferInputValidation: true })
+
+const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.fileUpload || {}) })({
+ dropzone: props.dropzone,
+ multiple: props.multiple,
+ size: props.size
+}))
const previewUrls = computed(() => Array.from(files.value || []).map(file => URL.createObjectURL(file)))
@@ -98,11 +125,48 @@ onChange((files) => {
onCancel(() => {
/** do something on cancel */
})
+
+function onDrop(files: File[] | null) {
+ modelValue.value = (props.multiple ? files : files?.[0]) as (M extends true ? File[] : File) | null
+}
-
+
+
+
+
+
+
+
+
+
+
+
+ {{ label }}
+
+
+
+
+ {{ description }}
+
+
+
+
+
+
+
+
+
+
+
{
:multiple="multiple"
:required="required"
:disabled="disabled"
- hidden
- tabindex="-1"
v-bind="{ ...$attrs, ...ariaAttrs }"
+ hidden
>
diff --git a/src/theme/file-upload.ts b/src/theme/file-upload.ts
index 269d3c46..71951eb5 100644
--- a/src/theme/file-upload.ts
+++ b/src/theme/file-upload.ts
@@ -1,11 +1,72 @@
-export default {
+import type { ModuleOptions } from '../module'
+
+export default (options: Required) => ({
slots: {
- root: '',
- base: ''
+ root: 'relative',
+ base: ['w-full bg-default hover:bg-elevated/25 border border-default p-4 flex flex-col items-center justify-center rounded-lg focus-visible:outline-primary', options.theme.transitions && 'transition-colors'],
+ wrapper: 'flex flex-col items-center justify-center text-center px-4 py-3',
+ leading: 'inline-flex items-center rounded-full ring ring-default',
+ leadingIcon: 'shrink-0 text-default',
+ label: 'font-medium text-default mt-2',
+ description: 'text-muted mt-1',
+ actions: 'flex flex-wrap gap-1.5 shrink-0',
+ preview: 'absolute inset-0'
},
variants: {
+ color: {
+ ...Object.fromEntries((options.theme.colors || []).map((color: string) => [color, ''])),
+ neutral: ''
+ },
+ size: {
+ xs: {
+ base: 'text-xs',
+ leading: 'p-1',
+ leadingIcon: 'size-4'
+ },
+ sm: {
+ base: 'text-xs',
+ leading: 'p-1.5',
+ leadingIcon: 'size-4'
+ },
+ md: {
+ base: 'text-sm',
+ leading: 'p-1.5',
+ leadingIcon: 'size-5'
+ },
+ lg: {
+ base: 'text-sm',
+ leading: 'p-2',
+ leadingIcon: 'size-5'
+ },
+ xl: {
+ base: 'text-base',
+ leading: 'p-2',
+ leadingIcon: 'size-6'
+ }
+ },
dropzone: {
- base: 'border-dashed'
+ true: 'border-dashed data-[dragging=true]:bg-elevated/25'
+ },
+ disabled: {
+ true: 'cursor-not-allowed opacity-75'
}
+ },
+ compoundVariants: [...(options.theme.colors || []).map((color: string) => ({
+ color,
+ class: `has-focus-visible:ring-2 has-focus-visible:ring-inset has-focus-visible:ring-${color}`
+ })), ...(options.theme.colors || []).map((color: string) => ({
+ color,
+ highlight: true,
+ class: `ring ring-inset ring-${color}`
+ })), {
+ color: 'neutral',
+ class: 'has-focus-visible:ring-2 has-focus-visible:ring-inset has-focus-visible:ring-inverted'
+ }, {
+ color: 'neutral',
+ highlight: true,
+ class: 'ring ring-inset ring-inverted'
+ }],
+ defaultVariants: {
+ size: 'md'
}
-}
+})
diff --git a/src/theme/icons.ts b/src/theme/icons.ts
index b0d6357b..bee424c1 100644
--- a/src/theme/icons.ts
+++ b/src/theme/icons.ts
@@ -16,5 +16,6 @@ export default {
loading: 'i-lucide-loader-circle',
minus: 'i-lucide-minus',
plus: 'i-lucide-plus',
- search: 'i-lucide-search'
+ search: 'i-lucide-search',
+ upload: 'i-lucide-upload'
}