feat(ui,stage-ui): FieldInput, InputFileCard, and story update

This commit is contained in:
Neko Ayaka
2026-04-10 03:40:09 +08:00
parent ec4d1998d7
commit 14f8275fa7
9 changed files with 255 additions and 55 deletions
@@ -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>