feat(ui,stage-ui): FieldInput, InputFileCard, and story update
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { FieldInputFile } from '@proj-airi/ui'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const files = ref<File[] | undefined>(undefined)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Field Input File"
|
||||
group="form"
|
||||
:layout="{ type: 'grid', width: '100%' }"
|
||||
>
|
||||
<template #controls>
|
||||
<ThemeColorsHueControl />
|
||||
</template>
|
||||
|
||||
<Variant
|
||||
id="default"
|
||||
title="Default"
|
||||
>
|
||||
<FieldInputFile
|
||||
v-model="files"
|
||||
label="Upload screenshot or video"
|
||||
description="Optionally attach files from your photo library."
|
||||
accept="image/*,video/*"
|
||||
multiple
|
||||
placeholder="Choose screenshot or video file"
|
||||
/>
|
||||
<div class="mt-2 text-xs text-neutral-500 dark:text-neutral-400">
|
||||
Selected: {{ files?.length ?? 0 }} file(s)
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { InputFileCard } from '@proj-airi/ui'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const files = ref<File[]>([])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Input File Card"
|
||||
group="form"
|
||||
:layout="{ type: 'grid', width: 300 }"
|
||||
>
|
||||
<template #controls>
|
||||
<ThemeColorsHueControl />
|
||||
</template>
|
||||
|
||||
<Variant id="default" title="Default">
|
||||
<InputFileCard v-model="files" />
|
||||
|
||||
<div p-2>
|
||||
<div>
|
||||
File List
|
||||
</div>
|
||||
<ul>
|
||||
<li v-for="file in files" :key="file.name">
|
||||
<p>
|
||||
{{ file.name }}, {{ file.size }} bytes, {{ file.type }}
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -2,33 +2,27 @@
|
||||
import { InputFile } from '@proj-airi/ui'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const files = ref<File[]>([])
|
||||
const files = ref<File[] | undefined>(undefined)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Input File"
|
||||
group="form"
|
||||
:layout="{ type: 'grid', width: 300 }"
|
||||
:layout="{ type: 'grid', width: 360 }"
|
||||
>
|
||||
<template #controls>
|
||||
<ThemeColorsHueControl />
|
||||
</template>
|
||||
|
||||
<Variant id="default" title="Default">
|
||||
<InputFile v-model="files" />
|
||||
<InputFile
|
||||
v-model="files"
|
||||
accept="image/*,video/*"
|
||||
/>
|
||||
|
||||
<div p-2>
|
||||
<div>
|
||||
File List
|
||||
</div>
|
||||
<ul>
|
||||
<li v-for="file in files" :key="file.name">
|
||||
<p>
|
||||
{{ file.name }}, {{ file.size }} bytes, {{ file.type }}
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="mt-2 text-xs text-neutral-500 dark:text-neutral-400">
|
||||
Selected: {{ files?.length ?? 0 }} file(s)
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import { InputFile } from '../input'
|
||||
|
||||
const props = defineProps<{
|
||||
label?: string
|
||||
description?: string
|
||||
accept?: string
|
||||
multiple?: boolean
|
||||
placeholder?: string
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<File[] | undefined>({ required: false })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="max-w-full">
|
||||
<label class="flex flex-col gap-4">
|
||||
<div>
|
||||
<div class="flex items-center gap-1 text-sm font-medium">
|
||||
<slot name="label">
|
||||
{{ props.label }}
|
||||
</slot>
|
||||
</div>
|
||||
<div class="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
<slot name="description">
|
||||
{{ props.description }}
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<InputFile
|
||||
v-model="modelValue"
|
||||
:accept="props.accept"
|
||||
:multiple="props.multiple"
|
||||
:placeholder="props.placeholder"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,5 +1,6 @@
|
||||
export { default as FieldCheckbox } from './field-checkbox.vue'
|
||||
export { default as FieldCombobox } from './field-combobox-select.vue'
|
||||
export { default as FieldInputFile } from './field-input-file.vue'
|
||||
export { default as FieldInput } from './field-input.vue'
|
||||
export { default as FieldKeyValues } from './field-key-values.vue'
|
||||
export { default as FieldRange } from './field-range.vue'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export { default as BasicInputFile } from './basic-input-file.vue'
|
||||
export { default as InputFileCard } from './input-file-card.vue'
|
||||
export { default as InputFile } from './input-file.vue'
|
||||
export { default as InputKeyValue } from './input-key-value.vue'
|
||||
export { default as Input } from './input.vue'
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
import { useSlots } from 'vue'
|
||||
|
||||
import BasicInputFile from './basic-input-file.vue'
|
||||
|
||||
defineProps<{
|
||||
accept?: string
|
||||
multiple?: boolean
|
||||
}>()
|
||||
|
||||
const slots = useSlots()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicInputFile
|
||||
:class="[
|
||||
'min-h-[120px] flex flex-col cursor-pointer items-center justify-center rounded-xl p-6',
|
||||
'border-dashed border-2',
|
||||
'transition-all duration-300',
|
||||
'opacity-95',
|
||||
'hover:scale-100 hover:opacity-100 hover:shadow-md hover:dark:shadow-lg',
|
||||
]"
|
||||
:is-not-dragging-classes="[
|
||||
'border-neutral-200 dark:border-neutral-700 hover:border-primary-300 dark:hover:border-primary-700',
|
||||
'bg-white/60 dark:bg-black/30 hover:bg-white/80 dark:hover:bg-black/40',
|
||||
]"
|
||||
:is-dragging-classes="[
|
||||
'border-primary-400 dark:border-primary-600 hover:border-primary-300 dark:hover:border-primary-700',
|
||||
'bg-primary-50/5 dark:bg-primary-900/5',
|
||||
]"
|
||||
:accept="accept"
|
||||
:multiple="multiple"
|
||||
>
|
||||
<template #default="{ isDragging }">
|
||||
<slot v-if="slots.default" :is-dragging="isDragging" />
|
||||
<div
|
||||
v-else
|
||||
class="flex flex-col items-center"
|
||||
:class="[
|
||||
isDragging ? 'text-primary-500 dark:text-primary-400' : 'text-neutral-400 dark:text-neutral-500',
|
||||
]"
|
||||
>
|
||||
<div i-solar:upload-square-line-duotone mb-2 text-5xl />
|
||||
<p font-medium text="center lg">
|
||||
Upload
|
||||
</p>
|
||||
<p v-if="isDragging" text="center" text-sm>
|
||||
Release to upload
|
||||
</p>
|
||||
<p v-else text="center" text-sm>
|
||||
Click or drag and drop a file here
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</BasicInputFile>
|
||||
</template>
|
||||
@@ -1,56 +1,96 @@
|
||||
<script setup lang="ts">
|
||||
import { useSlots } from 'vue'
|
||||
import { useObjectUrl } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import BasicInputFile from './basic-input-file.vue'
|
||||
|
||||
defineProps<{
|
||||
const props = withDefaults(defineProps<{
|
||||
accept?: string
|
||||
multiple?: boolean
|
||||
}>()
|
||||
placeholder?: string
|
||||
}>(), {
|
||||
placeholder: 'Choose file',
|
||||
multiple: false,
|
||||
})
|
||||
|
||||
const slots = useSlots()
|
||||
const modelValue = defineModel<File[] | undefined>({ default: undefined })
|
||||
|
||||
const fileNames = computed(() => {
|
||||
const files = modelValue.value ?? []
|
||||
if (!files.length)
|
||||
return props.placeholder
|
||||
return files.map(file => file.name).join(', ')
|
||||
})
|
||||
|
||||
const previewImageFile = computed(() => {
|
||||
const files = modelValue.value ?? []
|
||||
return files.find(file => file.type.startsWith('image/'))
|
||||
})
|
||||
|
||||
const previewUrl = useObjectUrl(previewImageFile)
|
||||
|
||||
function onFileChange(event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
if (!input.files) {
|
||||
modelValue.value = undefined
|
||||
return
|
||||
}
|
||||
|
||||
const files = Array.from(input.files)
|
||||
modelValue.value = files.length ? files : undefined
|
||||
|
||||
// Allow re-selecting the same file.
|
||||
input.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicInputFile
|
||||
<label
|
||||
:class="[
|
||||
'min-h-[120px] flex flex-col cursor-pointer items-center justify-center rounded-xl p-6',
|
||||
'border-dashed border-2',
|
||||
'transition-all duration-300',
|
||||
'opacity-95',
|
||||
'hover:scale-100 hover:opacity-100 hover:shadow-md hover:dark:shadow-lg',
|
||||
'w-full flex cursor-pointer items-center gap-2',
|
||||
'rounded-lg border-2 border-solid border-neutral-100 bg-neutral-50 px-2 py-1 shadow-sm',
|
||||
'transition-all duration-200 ease-in-out',
|
||||
'dark:border-neutral-900 dark:bg-neutral-950',
|
||||
'hover:border-primary-300/70 dark:hover:border-primary-700/70',
|
||||
]"
|
||||
:is-not-dragging-classes="[
|
||||
'border-neutral-200 dark:border-neutral-700 hover:border-primary-300 dark:hover:border-primary-700',
|
||||
'bg-white/60 dark:bg-black/30 hover:bg-white/80 dark:hover:bg-black/40',
|
||||
]"
|
||||
:is-dragging-classes="[
|
||||
'border-primary-400 dark:border-primary-600 hover:border-primary-300 dark:hover:border-primary-700',
|
||||
'bg-primary-50/5 dark:bg-primary-900/5',
|
||||
]"
|
||||
:accept="accept"
|
||||
:multiple="multiple"
|
||||
>
|
||||
<template #default="{ isDragging }">
|
||||
<slot v-if="slots.default" :is-dragging="isDragging" />
|
||||
<div
|
||||
v-else
|
||||
class="flex flex-col items-center"
|
||||
<input
|
||||
type="file"
|
||||
:accept="accept"
|
||||
:multiple="multiple"
|
||||
:class="[
|
||||
'hidden',
|
||||
]"
|
||||
@change="onFileChange"
|
||||
>
|
||||
|
||||
<div
|
||||
:class="[
|
||||
'i-solar:upload-square-line-duotone h-5 w-5 shrink-0 text-neutral-500 dark:text-neutral-400',
|
||||
]"
|
||||
/>
|
||||
|
||||
<div
|
||||
:class="[
|
||||
'min-w-0 flex-1 truncate text-sm text-neutral-600 dark:text-neutral-300',
|
||||
]"
|
||||
:title="fileNames"
|
||||
>
|
||||
{{ fileNames }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="previewUrl"
|
||||
:class="[
|
||||
'h-8 w-8 shrink-0 overflow-hidden rounded-md border border-neutral-200 bg-white',
|
||||
'dark:border-neutral-700 dark:bg-neutral-900',
|
||||
]"
|
||||
>
|
||||
<img
|
||||
:src="previewUrl"
|
||||
alt="Preview"
|
||||
:class="[
|
||||
isDragging ? 'text-primary-500 dark:text-primary-400' : 'text-neutral-400 dark:text-neutral-500',
|
||||
'h-full w-full object-cover',
|
||||
]"
|
||||
>
|
||||
<div i-solar:upload-square-line-duotone mb-2 text-5xl />
|
||||
<p font-medium text="center lg">
|
||||
Upload
|
||||
</p>
|
||||
<p v-if="isDragging" text="center" text-sm>
|
||||
Release to upload
|
||||
</p>
|
||||
<p v-else text="center" text-sm>
|
||||
Click or drag and drop a file here
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</BasicInputFile>
|
||||
</div>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user