파일 (File) 업로드 (Upload)
Usage
v-model 지시문을 사용하여 FileUpload 값을 제어합니다.
<script setup lang="ts">
const value = ref(null)
</script>
<template>
<UFileUpload v-model="value" class="w-96 min-h-48" />
</template><script setup lang="ts">
import { ref } from 'vue'
const value = ref(null)
</script>
<template>
<UFileUpload v-model="value" class="w-96 min-h-48" />
</template>Multiple 다중
multiple prop를 사용하여 여러 파일을 선택할 수 있습니다.
<template>
<UFileUpload multiple class="w-96 min-h-48" />
</template>Dropzone 이미지
dropzone 소품을 사용하여 드롭 가능 영역을 활성화/비활성화합니다. 기본값은 true입니다.
<template>
<UFileUpload :dropzone="false" class="w-96 min-h-48" />
</template>Interactive 대화식
interactive 소품을 사용하여 클릭 가능한 영역을 활성화/비활성화합니다. 기본값은 true입니다.
<template>
<UFileUpload :interactive="false" class="w-96 min-h-48" />
</template>수락 됨
accept 소품을 사용하여 입력에 허용되는 파일 유형을 지정합니다. MIME types 또는 파일 확장자(예: image/png,application/pdf,.jpg)의 쉼표로 구분된 목록을 제공합니다. 기본값은 *(모든 파일 유형)입니다.
<template>
<UFileUpload accept="image/*" class="w-96 min-h-48" />
</template>Label 태그
label prop을 사용하여 FileUpload 레이블을 설정합니다.
<template>
<UFileUpload label="Drop your image here" class="w-96 min-h-48" />
</template>Description
description prop를 사용하여 FileUpload에 대한 설명을 설정합니다.
<template>
<UFileUpload
label="Drop your image here"
description="SVG, PNG, JPG or GIF (max. 2MB)"
class="w-96 min-h-48"
/>
</template>Icon
icon prop을 사용하여 FileUpload.Defaults 아이콘을 i-lucide-upload로 설정합니다.
<template>
<UFileUpload
icon="i-lucide-image"
label="Drop your image here"
description="SVG, PNG, JPG or GIF (max. 2MB)"
class="w-96 min-h-48"
/>
</template>color
color prop을 사용하여 FileUpload 색상을 변경합니다.
<template>
<UFileUpload
color="neutral"
highlight
label="Drop your image here"
description="SVG, PNG, JPG or GIF (max. 2MB)"
class="w-96 min-h-48"
/>
</template>highlight prop은 초점 상태를 표시하기 위해 사용되며, 유효성 검사 오류가 발생할 때 내부적으로 사용됩니다.Variant
variant prop을 사용하여 FileUpload 변형을 변경합니다.
<template>
<UFileUpload variant="button" />
</template>Size
size prop을 사용하여 FileUpload 크기를 변경합니다.
<template>
<UFileUpload
size="xl"
variant="area"
label="Drop your image here"
description="SVG, PNG, JPG or GIF (max. 2MB)"
/>
</template>Layout
layout prop을 사용하여 FileUpload.Defaults에 파일이 표시되는 방식을 grid로 변경합니다.
variant가 area일 때만 작동합니다.<template>
<UFileUpload
layout="list"
multiple
label="Drop your images here"
description="SVG, PNG, JPG or GIF (max. 2MB)"
class="w-96"
:ui="{
base: 'min-h-48'
}"
/>
</template>Position 위치
position prop을 사용하여 FileUpload.Defaults에서 파일의 위치를 outside로 변경합니다.
variant가 area이고 layout가 list일 때만 작동합니다.<template>
<UFileUpload
position="inside"
layout="list"
multiple
label="Drop your images here"
description="SVG, PNG, JPG or GIF (max. 2MB)"
class="w-96"
:ui="{
base: 'min-h-48'
}"
/>
</template>예제
With 폼 유효성 검사
Form 및 FormField 구성 요소 내에서 FileUpload를 사용하여 유효성 검사 및 오류 처리를 처리할 수 있습니다.
<script setup lang="ts">
import * as z from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'
const MAX_FILE_SIZE = 2 * 1024 * 1024 // 2MB
const MIN_DIMENSIONS = { width: 200, height: 200 }
const MAX_DIMENSIONS = { width: 4096, height: 4096 }
const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']
const formatBytes = (bytes: number, decimals = 2) => {
if (bytes === 0) return '0 Bytes'
const k = 1024
const dm = decimals < 0 ? 0 : decimals
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
}
const schema = z.object({
image: z
.instanceof(File, {
message: 'Please select an image file.'
})
.refine((file) => file.size <= MAX_FILE_SIZE, {
message: `The image is too large. Please choose an image smaller than ${formatBytes(MAX_FILE_SIZE)}.`
})
.refine((file) => ACCEPTED_IMAGE_TYPES.includes(file.type), {
message: 'Please upload a valid image file (JPEG, PNG, or WebP).'
})
.refine(
(file) =>
new Promise((resolve) => {
const reader = new FileReader()
reader.onload = (e) => {
const img = new Image()
img.onload = () => {
const meetsDimensions =
img.width >= MIN_DIMENSIONS.width &&
img.height >= MIN_DIMENSIONS.height &&
img.width <= MAX_DIMENSIONS.width &&
img.height <= MAX_DIMENSIONS.height
resolve(meetsDimensions)
}
img.src = e.target?.result as string
}
reader.readAsDataURL(file)
}),
{
message: `The image dimensions are invalid. Please upload an image between ${MIN_DIMENSIONS.width}x${MIN_DIMENSIONS.height} and ${MAX_DIMENSIONS.width}x${MAX_DIMENSIONS.height} pixels.`
}
)
})
type Schema = z.output<typeof schema>
const state = reactive<Partial<Schema>>({
image: undefined
})
async function onSubmit(event: FormSubmitEvent<Schema>) {
console.log(event.data)
}
</script>
<template>
<UForm :schema="schema" :state="state" class="space-y-4 w-96" @submit="onSubmit">
<UFormField name="image" label="Image" description="JPG, GIF or PNG. 2MB Max.">
<UFileUpload v-model="state.image" accept="image/*" class="min-h-48" />
</UFormField>
<UButton type="submit" label="Submit" color="neutral" />
</UForm>
</template><script setup lang="ts">
import { reactive } from 'vue'
import * as z from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'
const MAX_FILE_SIZE = 2 * 1024 * 1024 // 2MB
const MIN_DIMENSIONS = { width: 200, height: 200 }
const MAX_DIMENSIONS = { width: 4096, height: 4096 }
const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']
const formatBytes = (bytes: number, decimals = 2) => {
if (bytes === 0) return '0 Bytes'
const k = 1024
const dm = decimals < 0 ? 0 : decimals
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
}
const schema = z.object({
image: z
.instanceof(File, {
message: 'Please select an image file.'
})
.refine((file) => file.size <= MAX_FILE_SIZE, {
message: `The image is too large. Please choose an image smaller than ${formatBytes(MAX_FILE_SIZE)}.`
})
.refine((file) => ACCEPTED_IMAGE_TYPES.includes(file.type), {
message: 'Please upload a valid image file (JPEG, PNG, or WebP).'
})
.refine(
(file) =>
new Promise((resolve) => {
const reader = new FileReader()
reader.onload = (e) => {
const img = new Image()
img.onload = () => {
const meetsDimensions =
img.width >= MIN_DIMENSIONS.width &&
img.height >= MIN_DIMENSIONS.height &&
img.width <= MAX_DIMENSIONS.width &&
img.height <= MAX_DIMENSIONS.height
resolve(meetsDimensions)
}
img.src = e.target?.result as string
}
reader.readAsDataURL(file)
}),
{
message: `The image dimensions are invalid. Please upload an image between ${MIN_DIMENSIONS.width}x${MIN_DIMENSIONS.height} and ${MAX_DIMENSIONS.width}x${MAX_DIMENSIONS.height} pixels.`
}
)
})
type Schema = z.output<typeof schema>
const state = reactive<Partial<Schema>>({
image: undefined
})
async function onSubmit(event: FormSubmitEvent<Schema>) {
console.log(event.data)
}
</script>
<template>
<UForm :schema="schema" :state="state" class="space-y-4 w-96" @submit="onSubmit">
<UFormField name="image" label="Image" description="JPG, GIF or PNG. 2MB Max.">
<UFileUpload v-model="state.image" accept="image/*" class="min-h-48" />
</UFormField>
<UButton type="submit" label="Submit" color="neutral" />
</UForm>
</template>기본 슬롯 사용
기본 슬롯을 사용하여 고유한 FileUpload 구성 요소를 만들 수 있습니다.
<script setup lang="ts">
import * as z from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'
const MAX_FILE_SIZE = 2 * 1024 * 1024 // 2MB
const MIN_DIMENSIONS = { width: 200, height: 200 }
const MAX_DIMENSIONS = { width: 4096, height: 4096 }
const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']
const formatBytes = (bytes: number, decimals = 2) => {
if (bytes === 0) return '0 Bytes'
const k = 1024
const dm = decimals < 0 ? 0 : decimals
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
}
const schema = z.object({
avatar: z
.instanceof(File, {
message: 'Please select an image file.'
})
.refine((file) => file.size <= MAX_FILE_SIZE, {
message: `The image is too large. Please choose an image smaller than ${formatBytes(MAX_FILE_SIZE)}.`
})
.refine((file) => ACCEPTED_IMAGE_TYPES.includes(file.type), {
message: 'Please upload a valid image file (JPEG, PNG, or WebP).'
})
.refine(
(file) =>
new Promise((resolve) => {
const reader = new FileReader()
reader.onload = (e) => {
const img = new Image()
img.onload = () => {
const meetsDimensions =
img.width >= MIN_DIMENSIONS.width &&
img.height >= MIN_DIMENSIONS.height &&
img.width <= MAX_DIMENSIONS.width &&
img.height <= MAX_DIMENSIONS.height
resolve(meetsDimensions)
}
img.src = e.target?.result as string
}
reader.readAsDataURL(file)
}),
{
message: `The image dimensions are invalid. Please upload an image between ${MIN_DIMENSIONS.width}x${MIN_DIMENSIONS.height} and ${MAX_DIMENSIONS.width}x${MAX_DIMENSIONS.height} pixels.`
}
)
})
type Schema = z.output<typeof schema>
const state = reactive<Partial<Schema>>({
avatar: undefined
})
function createObjectUrl(file: File): string {
return URL.createObjectURL(file)
}
async function onSubmit(event: FormSubmitEvent<Schema>) {
console.log(event.data)
}
</script>
<template>
<UForm :schema="schema" :state="state" class="space-y-4 w-64" @submit="onSubmit">
<UFormField name="avatar" label="Avatar" description="JPG, GIF or PNG. 1MB Max.">
<UFileUpload v-slot="{ open, removeFile }" v-model="state.avatar" accept="image/*">
<div class="flex flex-wrap items-center gap-3">
<UAvatar
size="lg"
:src="state.avatar ? createObjectUrl(state.avatar) : undefined"
icon="i-lucide-image"
/>
<UButton
:label="state.avatar ? 'Change image' : 'Upload image'"
color="neutral"
variant="outline"
@click="open()"
/>
</div>
<p v-if="state.avatar" class="text-xs text-muted mt-1.5">
{{ state.avatar.name }}
<UButton
label="Remove"
color="error"
variant="link"
size="xs"
class="p-0"
@click="removeFile()"
/>
</p>
</UFileUpload>
</UFormField>
<UButton type="submit" label="Submit" color="neutral" />
</UForm>
</template><script setup lang="ts">
import { reactive } from 'vue'
import * as z from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'
const MAX_FILE_SIZE = 2 * 1024 * 1024 // 2MB
const MIN_DIMENSIONS = { width: 200, height: 200 }
const MAX_DIMENSIONS = { width: 4096, height: 4096 }
const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']
const formatBytes = (bytes: number, decimals = 2) => {
if (bytes === 0) return '0 Bytes'
const k = 1024
const dm = decimals < 0 ? 0 : decimals
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
}
const schema = z.object({
avatar: z
.instanceof(File, {
message: 'Please select an image file.'
})
.refine((file) => file.size <= MAX_FILE_SIZE, {
message: `The image is too large. Please choose an image smaller than ${formatBytes(MAX_FILE_SIZE)}.`
})
.refine((file) => ACCEPTED_IMAGE_TYPES.includes(file.type), {
message: 'Please upload a valid image file (JPEG, PNG, or WebP).'
})
.refine(
(file) =>
new Promise((resolve) => {
const reader = new FileReader()
reader.onload = (e) => {
const img = new Image()
img.onload = () => {
const meetsDimensions =
img.width >= MIN_DIMENSIONS.width &&
img.height >= MIN_DIMENSIONS.height &&
img.width <= MAX_DIMENSIONS.width &&
img.height <= MAX_DIMENSIONS.height
resolve(meetsDimensions)
}
img.src = e.target?.result as string
}
reader.readAsDataURL(file)
}),
{
message: `The image dimensions are invalid. Please upload an image between ${MIN_DIMENSIONS.width}x${MIN_DIMENSIONS.height} and ${MAX_DIMENSIONS.width}x${MAX_DIMENSIONS.height} pixels.`
}
)
})
type Schema = z.output<typeof schema>
const state = reactive<Partial<Schema>>({
avatar: undefined
})
function createObjectUrl(file: File): string {
return URL.createObjectURL(file)
}
async function onSubmit(event: FormSubmitEvent<Schema>) {
console.log(event.data)
}
</script>
<template>
<UForm :schema="schema" :state="state" class="space-y-4 w-64" @submit="onSubmit">
<UFormField name="avatar" label="Avatar" description="JPG, GIF or PNG. 1MB Max.">
<UFileUpload v-slot="{ open, removeFile }" v-model="state.avatar" accept="image/*">
<div class="flex flex-wrap items-center gap-3">
<UAvatar
size="lg"
:src="state.avatar ? createObjectUrl(state.avatar) : undefined"
icon="i-lucide-image"
/>
<UButton
:label="state.avatar ? 'Change image' : 'Upload image'"
color="neutral"
variant="outline"
@click="open()"
/>
</div>
<p v-if="state.avatar" class="text-xs text-muted mt-1.5">
{{ state.avatar.name }}
<UButton
label="Remove"
color="error"
variant="link"
size="xs"
class="p-0"
@click="removeFile()"
/>
</p>
</UFileUpload>
</UFormField>
<UButton type="submit" label="Submit" color="neutral" />
</UForm>
</template>파일 하단 슬롯이 있습니다.
files-bottom 슬롯을 사용하여 파일 목록 아래에 Button를 추가하여 모든 파일을 제거 할 수 있습니다.
<script setup lang="ts">
const value = ref<File[]>([])
</script>
<template>
<UFileUpload
v-model="value"
icon="i-lucide-image"
label="Drop your images here"
description="SVG, PNG, JPG or GIF (max. 2MB)"
layout="list"
multiple
:interactive="false"
class="w-96 min-h-48"
>
<template #actions="{ open }">
<UButton
label="Select images"
icon="i-lucide-upload"
color="neutral"
variant="outline"
@click="open()"
/>
</template>
<template #files-bottom="{ removeFile, files }">
<UButton
v-if="files?.length"
label="Remove all files"
color="neutral"
@click="removeFile()"
/>
</template>
</UFileUpload>
</template><script setup lang="ts">
import { ref } from 'vue'
const value = ref<File[]>([])
</script>
<template>
<UFileUpload
v-model="value"
icon="i-lucide-image"
label="Drop your images here"
description="SVG, PNG, JPG or GIF (max. 2MB)"
layout="list"
multiple
:interactive="false"
class="w-96 min-h-48"
>
<template #actions="{ open }">
<UButton
label="Select images"
icon="i-lucide-upload"
color="neutral"
variant="outline"
@click="open()"
/>
</template>
<template #files-bottom="{ removeFile, files }">
<UButton
v-if="files?.length"
label="Remove all files"
color="neutral"
@click="removeFile()"
/>
</template>
</UFileUpload>
</template>파일 상단 슬롯이 있습니다.
files-top 슬롯을 사용하여 파일 목록 위에 Button를 추가하여 새 파일을 추가 할 수 있습니다.
<script setup lang="ts">
const value = ref<File[]>([])
</script>
<template>
<UFileUpload
v-model="value"
icon="i-lucide-image"
label="Drop your images here"
description="SVG, PNG, JPG or GIF (max. 2MB)"
layout="grid"
multiple
:interactive="false"
class="w-96 min-h-48"
>
<template #actions="{ open }">
<UButton
label="Select images"
icon="i-lucide-upload"
color="neutral"
variant="outline"
@click="open()"
/>
</template>
<template #files-top="{ open, files }">
<div v-if="files?.length" class="mb-2 flex items-center justify-between">
<p class="font-bold">Files ({{ files?.length }})</p>
<UButton
icon="i-lucide-plus"
label="Add more"
color="neutral"
variant="outline"
class="-my-2"
@click="open()"
/>
</div>
</template>
</UFileUpload>
</template><script setup lang="ts">
import { ref } from 'vue'
const value = ref<File[]>([])
</script>
<template>
<UFileUpload
v-model="value"
icon="i-lucide-image"
label="Drop your images here"
description="SVG, PNG, JPG or GIF (max. 2MB)"
layout="grid"
multiple
:interactive="false"
class="w-96 min-h-48"
>
<template #actions="{ open }">
<UButton
label="Select images"
icon="i-lucide-upload"
color="neutral"
variant="outline"
@click="open()"
/>
</template>
<template #files-top="{ open, files }">
<div v-if="files?.length" class="mb-2 flex items-center justify-between">
<p class="font-bold">Files ({{ files?.length }})</p>
<UButton
icon="i-lucide-plus"
label="Add more"
color="neutral"
variant="outline"
class="-my-2"
@click="open()"
/>
</div>
</template>
</UFileUpload>
</template>API
Props (### Props)
| Prop | Default | Type |
|---|---|---|
as | 'div' | anyThe element or component this component should render as. |
id | string | |
name | string | |
icon | appConfig.ui.icons.upload | anyThe icon to display. Set to |
label | string | |
description | string | |
color | 'primary' | "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral" |
variant | 'area' | "button" | "area"The |
size | 'md' | "xs" | "sm" | "md" | "lg" | "xl" |
layout | 'grid' | "list" | "grid"The layout of how files are displayed.
Only works when |
position | 'outside' | "inside" | "outside"The position of the files.
Only works when |
highlight | boolean Highlight the ring color like a focus state. | |
accept | '*' | stringSpecifies the allowed file types for the input. Provide a comma-separated list of MIME types or file extensions (e.g., "image/png,application/pdf,.jpg"). |
multiple | false | M |
reset | false | boolean Reset the file input when the dialog is opened. |
dropzone | true | boolean Create a zone that allows the user to drop files onto it. |
interactive | true | boolean Make the dropzone interactive when the user is clicking on it. |
required | boolean | |
disabled | boolean | |
fileIcon | appConfig.ui.icons.file | anyThe icon to display for the file. |
fileImage | true | boolean Preview the file (currently only |
fileDelete | true | boolean | Omit<ButtonProps, LinkPropsKeys> Configure the delete button for the file.
When |
fileDeleteIcon | appConfig.ui.icons.close | anyThe icon displayed to delete a file. |
preview | true | boolean Show the file preview/list after upload. |
modelValue | null | M extends true ? File[] : File | |
ui | { root?: SlotClass; base?: SlotClass; wrapper?: SlotClass; icon?: SlotClass; avatar?: SlotClass; label?: SlotClass; description?: SlotClass; actions?: SlotClass; files?: SlotClass; file?: SlotClass; fileLeadingAvatar?: SlotClass; fileWrapper?: SlotClass; fileName?: SlotClass; fileSize?: SlotClass; fileTrailingButton?: SlotClass; } |
Slots
| Slot | Type |
|---|---|
default | { open: (localOptions?: Partial<UseFileDialogOptions> | undefined) => void; removeFile: (index?: number | undefined) => void; ui: object; } |
leading | { ui: object; } |
label | {} |
description | {} |
actions | { files: FileUploadFiles<M> | undefined; open: (localOptions?: Partial<UseFileDialogOptions> | undefined) => void; removeFile: (index?: number | undefined) => void; } |
files | { files: FileUploadFiles<M>; removeFile: (index?: number | undefined) => void; } |
files-top | { files: FileUploadFiles<M>; open: (localOptions?: Partial<UseFileDialogOptions> | undefined) => void; removeFile: (index?: number | undefined) => void; } |
files-bottom | { files: FileUploadFiles<M>; open: (localOptions?: Partial<UseFileDialogOptions> | undefined) => void; removeFile: (index?: number | undefined) => void; } |
file | { file: File; index: number; removeFile: (index?: number | undefined) => void; } |
file-leading | { file: File; index: number; ui: object; } |
file-name | { file: File; index: number; } |
file-size | { file: File; index: number; } |
file-trailing | { file: File; index: number; ui: object; } |
Emits
| Event | Type |
|---|---|
change | [event: Event] |
update:modelValue | [value: (M extends true ? File[] : File) | null | undefined] |
노출
템플릿 참조를 통해 컴포넌트에 액세스하는 경우 다음을 사용할 수 있습니다.
| 이름 Name | 유형 (Type) |
|---|---|
inputRef 파일 | Ref<HTMLInputElement | null> |
dropzoneRef 공식 | Ref<HTMLDivElement | null> 파일 |
Theme 테마
export default defineAppConfig({
ui: {
fileUpload: {
slots: {
root: 'relative flex flex-col',
base: [
'w-full flex-1 bg-default border border-default flex flex-col gap-2 items-stretch justify-center rounded-lg focus-visible:outline-3',
'transition-[background] ease-out'
],
wrapper: 'flex flex-col items-center justify-center text-center',
icon: 'shrink-0',
avatar: 'shrink-0',
label: 'font-medium text-default mt-2',
description: 'text-muted mt-1',
actions: 'flex flex-wrap gap-1.5 shrink-0 mt-4',
files: '',
file: 'relative',
fileLeadingAvatar: 'shrink-0',
fileWrapper: 'flex flex-col min-w-0',
fileName: 'text-default truncate',
fileSize: 'text-muted truncate',
fileTrailingButton: ''
},
variants: {
color: {
primary: '',
secondary: '',
success: '',
info: '',
warning: '',
error: '',
neutral: ''
},
variant: {
area: {
wrapper: 'px-4 py-3',
base: 'p-4'
},
button: {}
},
size: {
xs: {
base: 'text-xs',
icon: 'size-4',
file: 'text-xs px-2 py-1 gap-1',
fileWrapper: 'flex-row gap-1'
},
sm: {
base: 'text-xs',
icon: 'size-4',
file: 'text-xs px-2.5 py-1.5 gap-1.5',
fileWrapper: 'flex-row gap-1'
},
md: {
base: 'text-sm',
icon: 'size-5',
file: 'text-xs px-2.5 py-1.5 gap-1.5'
},
lg: {
base: 'text-sm',
icon: 'size-5',
file: 'text-sm px-3 py-2 gap-2',
fileSize: 'text-xs'
},
xl: {
base: 'text-base',
icon: 'size-6',
file: 'text-sm px-3 py-2 gap-2'
}
},
layout: {
list: {
root: 'gap-2 items-start',
files: 'flex flex-col w-full gap-2',
file: 'min-w-0 flex items-center border border-default rounded-md w-full',
fileTrailingButton: 'ms-auto'
},
grid: {
fileWrapper: 'hidden',
fileLeadingAvatar: 'size-full rounded-lg',
fileTrailingButton: 'absolute -top-1.5 -end-1.5 p-0 rounded-full border-2 border-bg'
}
},
position: {
inside: '',
outside: ''
},
dropzone: {
true: 'border-dashed data-[dragging=true]:bg-elevated/25'
},
interactive: {
true: ''
},
highlight: {
true: ''
},
multiple: {
true: ''
},
disabled: {
true: 'cursor-not-allowed opacity-75'
}
},
compoundVariants: [
{
color: 'primary',
class: 'outline-primary/25 focus-visible:outline-3 focus-visible:border-primary'
},
{
color: 'primary',
highlight: true,
class: 'border-primary'
},
{
color: 'neutral',
class: 'outline-inverted/25 focus-visible:outline-3 focus-visible:border-inverted'
},
{
color: 'neutral',
highlight: true,
class: 'border-inverted'
},
{
size: 'xs',
layout: 'list',
class: {
fileTrailingButton: '-me-1'
}
},
{
size: 'sm',
layout: 'list',
class: {
fileTrailingButton: '-me-1.5'
}
},
{
size: 'md',
layout: 'list',
class: {
fileTrailingButton: '-me-1.5'
}
},
{
size: 'lg',
layout: 'list',
class: {
fileTrailingButton: '-me-2'
}
},
{
size: 'xl',
layout: 'list',
class: {
fileTrailingButton: '-me-2'
}
},
{
variant: 'button',
size: 'xs',
class: {
base: 'p-1'
}
},
{
variant: 'button',
size: 'sm',
class: {
base: 'p-1.5'
}
},
{
variant: 'button',
size: 'md',
class: {
base: 'p-1.5'
}
},
{
variant: 'button',
size: 'lg',
class: {
base: 'p-2'
}
},
{
variant: 'button',
size: 'xl',
class: {
base: 'p-2'
}
},
{
layout: 'grid',
multiple: true,
class: {
files: 'grid grid-cols-2 md:grid-cols-3 gap-4 w-full',
file: 'p-0 aspect-square'
}
},
{
layout: 'grid',
multiple: false,
class: {
file: 'absolute inset-0 p-0'
}
},
{
interactive: true,
disabled: false,
class: 'hover:bg-elevated/25'
}
],
defaultVariants: {
color: 'primary',
variant: 'area',
size: 'md'
}
}
}
})import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: [
vue(),
ui({
ui: {
fileUpload: {
slots: {
root: 'relative flex flex-col',
base: [
'w-full flex-1 bg-default border border-default flex flex-col gap-2 items-stretch justify-center rounded-lg focus-visible:outline-3',
'transition-[background] ease-out'
],
wrapper: 'flex flex-col items-center justify-center text-center',
icon: 'shrink-0',
avatar: 'shrink-0',
label: 'font-medium text-default mt-2',
description: 'text-muted mt-1',
actions: 'flex flex-wrap gap-1.5 shrink-0 mt-4',
files: '',
file: 'relative',
fileLeadingAvatar: 'shrink-0',
fileWrapper: 'flex flex-col min-w-0',
fileName: 'text-default truncate',
fileSize: 'text-muted truncate',
fileTrailingButton: ''
},
variants: {
color: {
primary: '',
secondary: '',
success: '',
info: '',
warning: '',
error: '',
neutral: ''
},
variant: {
area: {
wrapper: 'px-4 py-3',
base: 'p-4'
},
button: {}
},
size: {
xs: {
base: 'text-xs',
icon: 'size-4',
file: 'text-xs px-2 py-1 gap-1',
fileWrapper: 'flex-row gap-1'
},
sm: {
base: 'text-xs',
icon: 'size-4',
file: 'text-xs px-2.5 py-1.5 gap-1.5',
fileWrapper: 'flex-row gap-1'
},
md: {
base: 'text-sm',
icon: 'size-5',
file: 'text-xs px-2.5 py-1.5 gap-1.5'
},
lg: {
base: 'text-sm',
icon: 'size-5',
file: 'text-sm px-3 py-2 gap-2',
fileSize: 'text-xs'
},
xl: {
base: 'text-base',
icon: 'size-6',
file: 'text-sm px-3 py-2 gap-2'
}
},
layout: {
list: {
root: 'gap-2 items-start',
files: 'flex flex-col w-full gap-2',
file: 'min-w-0 flex items-center border border-default rounded-md w-full',
fileTrailingButton: 'ms-auto'
},
grid: {
fileWrapper: 'hidden',
fileLeadingAvatar: 'size-full rounded-lg',
fileTrailingButton: 'absolute -top-1.5 -end-1.5 p-0 rounded-full border-2 border-bg'
}
},
position: {
inside: '',
outside: ''
},
dropzone: {
true: 'border-dashed data-[dragging=true]:bg-elevated/25'
},
interactive: {
true: ''
},
highlight: {
true: ''
},
multiple: {
true: ''
},
disabled: {
true: 'cursor-not-allowed opacity-75'
}
},
compoundVariants: [
{
color: 'primary',
class: 'outline-primary/25 focus-visible:outline-3 focus-visible:border-primary'
},
{
color: 'primary',
highlight: true,
class: 'border-primary'
},
{
color: 'neutral',
class: 'outline-inverted/25 focus-visible:outline-3 focus-visible:border-inverted'
},
{
color: 'neutral',
highlight: true,
class: 'border-inverted'
},
{
size: 'xs',
layout: 'list',
class: {
fileTrailingButton: '-me-1'
}
},
{
size: 'sm',
layout: 'list',
class: {
fileTrailingButton: '-me-1.5'
}
},
{
size: 'md',
layout: 'list',
class: {
fileTrailingButton: '-me-1.5'
}
},
{
size: 'lg',
layout: 'list',
class: {
fileTrailingButton: '-me-2'
}
},
{
size: 'xl',
layout: 'list',
class: {
fileTrailingButton: '-me-2'
}
},
{
variant: 'button',
size: 'xs',
class: {
base: 'p-1'
}
},
{
variant: 'button',
size: 'sm',
class: {
base: 'p-1.5'
}
},
{
variant: 'button',
size: 'md',
class: {
base: 'p-1.5'
}
},
{
variant: 'button',
size: 'lg',
class: {
base: 'p-2'
}
},
{
variant: 'button',
size: 'xl',
class: {
base: 'p-2'
}
},
{
layout: 'grid',
multiple: true,
class: {
files: 'grid grid-cols-2 md:grid-cols-3 gap-4 w-full',
file: 'p-0 aspect-square'
}
},
{
layout: 'grid',
multiple: false,
class: {
file: 'absolute inset-0 p-0'
}
},
{
interactive: true,
disabled: false,
class: 'hover:bg-elevated/25'
}
],
defaultVariants: {
color: 'primary',
variant: 'area',
size: 'md'
}
}
}
})
]
})