Add component and apply to import
This commit is contained in:
@ -4,7 +4,7 @@ import { cva } from "class-variance-authority"
|
||||
export { default as Button } from "./Button.vue"
|
||||
|
||||
export const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
131
app/components/ui/file-upload/FileUpload.vue
Normal file
131
app/components/ui/file-upload/FileUpload.vue
Normal file
@ -0,0 +1,131 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import { Motion } from "motion-v";
|
||||
import { ref } from "vue";
|
||||
|
||||
interface FileUploadProps {
|
||||
class?: HTMLAttributes["class"];
|
||||
accept?: string;
|
||||
}
|
||||
|
||||
defineProps<FileUploadProps>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "onChange", files: File[]): void;
|
||||
}>();
|
||||
|
||||
const fileInputRef = ref<HTMLInputElement | null>(null);
|
||||
const files = ref<File[]>([]);
|
||||
const isActive = ref<boolean>(false);
|
||||
|
||||
function handleFileChange(newFiles: File[]) {
|
||||
files.value = [...files.value, ...newFiles];
|
||||
emit("onChange", files.value);
|
||||
}
|
||||
|
||||
function onFileChange(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
if (!input.files) return;
|
||||
handleFileChange(Array.from(input.files));
|
||||
}
|
||||
|
||||
function handleClick() {
|
||||
fileInputRef.value?.click();
|
||||
}
|
||||
|
||||
function handleEnter() {
|
||||
isActive.value = true;
|
||||
}
|
||||
function handleLeave() {
|
||||
isActive.value = false;
|
||||
}
|
||||
function handleDrop(e: DragEvent) {
|
||||
isActive.value = false;
|
||||
const droppedFiles = e.dataTransfer?.files ? Array.from(e.dataTransfer.files) : [];
|
||||
if (droppedFiles.length) handleFileChange(droppedFiles);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ClientOnly>
|
||||
<div class="w-full" :class="[$props.class]" @dragover.prevent="handleEnter" @dragleave="handleLeave"
|
||||
@drop.prevent="handleDrop" @mouseover="handleEnter" @mouseleave="handleLeave">
|
||||
<div class="group/file relative block w-full cursor-pointer overflow-hidden rounded-lg p-10 bg-muted space-y-4"
|
||||
@click="handleClick">
|
||||
<input ref="fileInputRef" type="file" class="hidden" :accept="accept" @change="onFileChange" />
|
||||
|
||||
<!-- Grid pattern -->
|
||||
<div
|
||||
class="pointer-events-none absolute inset-0 [mask-image:radial-gradient(ellipse_at_center,white,transparent)]">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<div class="relative mx-auto mt-10 w-full max-w-xl space-y-4">
|
||||
<Motion v-for="(file, idx) in files" :key="`file-${idx}`" :initial="{ opacity: 0, scaleX: 0 }"
|
||||
:animate="{ opacity: 1, scaleX: 1 }"
|
||||
class="relative z-40 mx-auto flex w-full flex-col items-start justify-start overflow-hidden rounded-md p-4 shadow-sm md:h-24 text-muted-foreground bg-background">
|
||||
<div class="flex w-full items-center justify-between gap-4">
|
||||
<Motion as="p" :initial="{ opacity: 0 }" :animate="{ opacity: 1 }"
|
||||
class="max-w-xs truncate text-base">
|
||||
{{ file.name }}
|
||||
</Motion>
|
||||
<Motion as="p" :initial="{ opacity: 0 }" :animate="{ opacity: 1 }"
|
||||
class="shadow-input w-fit shrink-0 rounded-lg px-2 py-1 text-sm">
|
||||
{{ (file.size / (1024 * 1024)).toFixed(2) }} MB
|
||||
</Motion>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-2 flex w-full flex-col items-start justify-between text-sm text-muted-foreground md:flex-row md:items-center">
|
||||
<Motion as="p" :initial="{ opacity: 0 }" :animate="{ opacity: 1 }"
|
||||
class="rounded-md bg-muted px-1.5 py-1 text-sm">
|
||||
{{ file.type || "unknown type" }}
|
||||
</Motion>
|
||||
</div>
|
||||
</Motion>
|
||||
|
||||
<template v-if="!files.length">
|
||||
<Motion as="div"
|
||||
class="relative z-40 mx-auto mt-4 flex h-32 w-full max-w-32 items-center justify-center rounded-md shadow-[0px_10px_50px_rgba(0,0,0,0.1)] bg-background group-hover/file:shadow-2xl"
|
||||
:initial="{
|
||||
x: 0,
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
}" :transition="{
|
||||
type: 'spring',
|
||||
stiffness: 300,
|
||||
damping: 20,
|
||||
}" :animate="isActive
|
||||
? {
|
||||
x: 20,
|
||||
y: -20,
|
||||
opacity: 0.9,
|
||||
}
|
||||
: {}
|
||||
">
|
||||
<Icon name="heroicons:arrow-up-tray-20-solid" size="20" />
|
||||
</Motion>
|
||||
|
||||
<div class="absolute inset-0 z-30 mx-auto mt-4 flex h-32 w-full max-w-32 items-center justify-center rounded-md border border-dashed border-border bg-transparent transition-opacity"
|
||||
:class="{ 'opacity-100': isActive, 'opacity-0': !isActive }" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex flex-col items-center justify-center gap-2">
|
||||
<slot name="content" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ClientOnly>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.group-hover\/file\:shadow-2xl:hover {
|
||||
box-shadow: 0px 10px 20px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.transition-opacity {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
</style>
|
||||
35
app/components/ui/file-upload/FileUploadGrid.vue
Normal file
35
app/components/ui/file-upload/FileUploadGrid.vue
Normal file
@ -0,0 +1,35 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from "vue";
|
||||
|
||||
interface FileUploadGridProps {
|
||||
class?: HTMLAttributes["class"];
|
||||
}
|
||||
|
||||
defineProps<FileUploadGridProps>();
|
||||
|
||||
const ROWS = 11;
|
||||
const COLUMNS = 41;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex shrink-0 scale-105 flex-wrap items-center justify-center gap-px bg-gray-100 dark:bg-neutral-900"
|
||||
:class="[$props.class]"
|
||||
>
|
||||
<template v-for="row in ROWS">
|
||||
<template
|
||||
v-for="col in COLUMNS"
|
||||
:key="`${row}-${col}`"
|
||||
>
|
||||
<div
|
||||
class="flex h-10 w-10 flex-shrink-0 rounded-[2px]"
|
||||
:class="[
|
||||
((row - 1) * COLUMNS + (col - 1)) % 2 === 0
|
||||
? 'bg-gray-50 dark:bg-neutral-950'
|
||||
: 'bg-gray-50 shadow-[0px_0px_1px_3px_rgba(255,255,255,1)_inset] dark:bg-neutral-950 dark:shadow-[0px_0px_1px_3px_rgba(0,0,0,1)_inset]',
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
2
app/components/ui/file-upload/index.ts
Normal file
2
app/components/ui/file-upload/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as FileUpload } from "./FileUpload.vue";
|
||||
export { default as FileUploadGrid } from "./FileUploadGrid.vue";
|
||||
Reference in New Issue
Block a user