mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-14 12:14:41 +01:00
up
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import * as z from 'zod'
|
||||
import type { FormSubmitEvent } from '@nuxt/ui'
|
||||
import theme from '#build/ui/file-upload'
|
||||
|
||||
const sizes = Object.keys(theme.variants.size) as Array<keyof typeof theme.variants.size>
|
||||
|
||||
const MAX_FILE_SIZE = 2 * 1024 * 1024 // 2MB
|
||||
const MIN_DIMENSIONS = { width: 200, height: 200 }
|
||||
@@ -64,41 +67,10 @@ async function onSubmit(event: FormSubmitEvent<schema>) {
|
||||
|
||||
console.log(res)
|
||||
}
|
||||
|
||||
const files = ref<File[]>([
|
||||
{
|
||||
name: 'image-01.jpg',
|
||||
size: 1528737,
|
||||
type: 'image/jpeg',
|
||||
url: 'https://picsum.photos/1000/800?grayscale&random=1',
|
||||
id: 'image-01-123456789'
|
||||
},
|
||||
{
|
||||
name: 'image-02.jpg',
|
||||
size: 1528737,
|
||||
type: 'image/jpeg',
|
||||
url: 'https://picsum.photos/1000/800?grayscale&random=2',
|
||||
id: 'image-02-123456789'
|
||||
},
|
||||
{
|
||||
name: 'image-03.jpg',
|
||||
size: 1528737,
|
||||
type: 'image/jpeg',
|
||||
url: 'https://picsum.photos/1000/800?grayscale&random=3',
|
||||
id: 'image-03-123456789'
|
||||
},
|
||||
{
|
||||
name: 'image-04.jpg',
|
||||
size: 1528737,
|
||||
type: 'image/jpeg',
|
||||
url: 'https://picsum.photos/1000/800?grayscale&random=4',
|
||||
id: 'image-04-123456789'
|
||||
}
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col items-center gap-8">
|
||||
<UForm :schema="schema" :state="state" class="space-y-4 w-80" @submit="onSubmit">
|
||||
<UFormField name="avatar" label="Avatar" description="JPG, GIF or PNG. 1MB Max.">
|
||||
<UFileUpload v-slot="{ open, reset, previewUrls }" v-model="state.avatar" accept="image/*">
|
||||
@@ -126,10 +98,14 @@ const files = ref<File[]>([
|
||||
<UButton label="Submit" type="submit" />
|
||||
</UForm>
|
||||
|
||||
<UFileUpload v-slot="{ files, open }" multiple>
|
||||
{{ files?.[0]?.name }}
|
||||
|
||||
<UButton label="Upload" color="neutral" @click="open()" />
|
||||
</UFileUpload>
|
||||
<div class="flex items-center overflow-x-auto gap-4">
|
||||
<UFileUpload
|
||||
v-for="size in sizes"
|
||||
:key="size"
|
||||
:size="size"
|
||||
label="Drop your image here"
|
||||
description="SVG, PNG, JPG or GIF (max. 2MB)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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<typeof theme, AppConfig, 'fileUpload'>
|
||||
@@ -14,6 +16,19 @@ export interface FileUploadProps<M extends boolean = false> {
|
||||
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<M extends boolean = false> {
|
||||
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
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -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<FileUploadProps<M>>(), {
|
||||
accept: '*'
|
||||
accept: '*',
|
||||
dropzone: true
|
||||
})
|
||||
defineEmits<FileUploadEmits<M>>()
|
||||
defineSlots<FileUploadSlots>()
|
||||
const slots = defineSlots<FileUploadSlots>()
|
||||
|
||||
const modelValue = defineModel<(M extends true ? File[] : File) | null>()
|
||||
|
||||
const appConfig = useAppConfig() as FileUpload['AppConfig']
|
||||
|
||||
const { emitFormInput, emitFormChange, id, name, disabled, ariaAttrs } = useFormField<FileUploadProps>(props, { deferInputValidation: true })
|
||||
|
||||
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.fileUpload || {}) })({
|
||||
dropzone: props.dropzone
|
||||
}))
|
||||
|
||||
const inputRef = ref<HTMLInputElement>()
|
||||
const dropZoneRef = ref<HTMLDivElement>()
|
||||
|
||||
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<FileUploadProps>(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
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive :as="as" :class="ui.root({ class: [props.ui?.root, props.class] })">
|
||||
<slot :open="open" :files="files" :reset="reset" :preview-urls="previewUrls" />
|
||||
<slot :open="open" :reset="reset" :preview-urls="previewUrls">
|
||||
<div
|
||||
ref="dropZoneRef"
|
||||
role="button"
|
||||
:data-dragging="isOverDropZone"
|
||||
:class="ui.base({ class: props.ui?.base })"
|
||||
@click="open()"
|
||||
>
|
||||
<div :class="ui.wrapper({ class: props.ui?.wrapper })">
|
||||
<div :class="ui.leading({ class: props.ui?.leading })">
|
||||
<slot name="leading">
|
||||
<UIcon :name="icon || appConfig.ui.icons.upload" :class="ui.leadingIcon({ class: props.ui?.icon })" />
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<div v-if="label || !!slots.label" :class="ui.label({ class: props.ui?.label })">
|
||||
<slot name="label">
|
||||
{{ label }}
|
||||
</slot>
|
||||
</div>
|
||||
<div v-if="description || !!slots.description" :class="ui.description({ class: props.ui?.description })">
|
||||
<slot name="description">
|
||||
{{ description }}
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<div v-if="actions?.length || !!slots.actions" :class="ui.actions({ class: props.ui?.actions })">
|
||||
<slot name="actions">
|
||||
<UButton v-for="(action, index) in actions" :key="index" size="xs" v-bind="action" />
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</slot>
|
||||
|
||||
<input
|
||||
:id="id"
|
||||
@@ -113,9 +177,8 @@ onCancel(() => {
|
||||
:multiple="multiple"
|
||||
:required="required"
|
||||
:disabled="disabled"
|
||||
hidden
|
||||
tabindex="-1"
|
||||
v-bind="{ ...$attrs, ...ariaAttrs }"
|
||||
hidden
|
||||
>
|
||||
</Primitive>
|
||||
</template>
|
||||
|
||||
@@ -1,11 +1,72 @@
|
||||
export default {
|
||||
import type { ModuleOptions } from '../module'
|
||||
|
||||
export default (options: Required<ModuleOptions>) => ({
|
||||
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'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user