feat(SPA): media field type
This commit is contained in:
1 parent
a2d596f4ca
commit
3e7fc9a4a5
57 files changed
+6109
-75
No files matched your search
@@ -8,6 +8,7 @@ import VariantPickerField from '@/components/variants/VariantPickerField.vue'
|
||||
import type { LibraryStatus } from '@/composables/useLibraryList'
|
||||
import type { ImageVariant } from '@/composables/useImageVariants'
|
||||
import type { MediaItem } from '@/composables/useMedia'
|
||||
import type { FieldPick } from '@/lib/imageChoice'
|
||||
import { accessibleName, formatDimensions, formatSize } from '@/lib/mediaFormat'
|
||||
|
||||
const SEARCH_DEBOUNCE_MS = 120
|
||||
@@ -41,11 +42,14 @@ const props = defineProps<{
|
||||
plain?: boolean
|
||||
tileRatio?: 'square' | 'landscape'
|
||||
hint?: string
|
||||
field?: FieldPick
|
||||
noneChosen?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [query: string]
|
||||
select: [item: MediaItem]
|
||||
'select-none': []
|
||||
'load-more': []
|
||||
retry: []
|
||||
upload: []
|
||||
@@ -71,12 +75,19 @@ const searching = computed(() => props.query !== '')
|
||||
const summary = computed(() =>
|
||||
searching.value ? t('Newest first') : t('Newest first · {0} images', [props.total]),
|
||||
)
|
||||
const chosen = computed(() => props.selected !== null || props.noneChosen)
|
||||
const scrollHeight = computed(() => {
|
||||
if (props.plain) {
|
||||
return 'max-h-71 md:max-h-89'
|
||||
}
|
||||
return props.selected ? 'max-h-32 md:max-h-49.5' : 'max-h-77'
|
||||
return chosen.value ? 'max-h-32 md:max-h-49.5' : 'max-h-77'
|
||||
})
|
||||
const showNoImage = computed(() => props.field?.optional === true && showGrid.value)
|
||||
const emptyDescription = computed(() =>
|
||||
props.field
|
||||
? t('Images you upload appear here, ready to use in any article or entry.')
|
||||
: t('Images you upload appear here, ready to use in any article.'),
|
||||
)
|
||||
const showEmptyLibrary = computed(
|
||||
() => props.status === 'ready' && props.items.length === 0 && !searching.value,
|
||||
)
|
||||
@@ -85,7 +96,7 @@ const showNoResults = computed(
|
||||
)
|
||||
const showGrid = computed(() => props.status === 'ready' && props.items.length > 0)
|
||||
const showLoadMore = computed(
|
||||
() => showGrid.value && props.hasMore && (props.plain || !props.selected),
|
||||
() => showGrid.value && props.hasMore && (props.plain || !chosen.value),
|
||||
)
|
||||
const noResultsTitle = computed(() => t('No images match \u0022{0}\u0022', [props.query]))
|
||||
|
||||
@@ -115,7 +126,19 @@ function isSelected(item: MediaItem) {
|
||||
}
|
||||
|
||||
function isInArticle(item: MediaItem) {
|
||||
return props.inArticleIds.includes(item.id)
|
||||
return !props.field && props.inArticleIds.includes(item.id)
|
||||
}
|
||||
|
||||
function isCurrent(item: MediaItem) {
|
||||
return props.field?.currentId === item.id
|
||||
}
|
||||
|
||||
function tileName(item: MediaItem) {
|
||||
if (!props.field) {
|
||||
return accessibleName(item)
|
||||
}
|
||||
const name = `${item.filename}, ${formatSize(item.size)}`
|
||||
return isCurrent(item) ? `${name}, ${t('Current')}` : name
|
||||
}
|
||||
|
||||
function onTileKeydown(event: KeyboardEvent, item: MediaItem) {
|
||||
@@ -206,7 +229,7 @@ onBeforeUnmount(clearDebounce)
|
||||
variant="naked"
|
||||
icon="i-lucide-images"
|
||||
:title="t('Your library is empty')"
|
||||
:description="t('Images you upload appear here, ready to use in any article.')"
|
||||
:description="emptyDescription"
|
||||
:ui="EMPTY_STATE_UI"
|
||||
>
|
||||
<template #actions>
|
||||
@@ -253,6 +276,26 @@ onBeforeUnmount(clearDebounce)
|
||||
</div>
|
||||
|
||||
<template v-else-if="showGrid">
|
||||
<button
|
||||
v-if="showNoImage"
|
||||
type="button"
|
||||
data-testid="picker-no-image"
|
||||
:aria-pressed="noneChosen"
|
||||
class="flex min-h-10 items-center gap-2.5 border px-3 text-start focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary max-md:min-h-12"
|
||||
:class="
|
||||
noneChosen
|
||||
? 'border-primary bg-(--rc-sand) text-(--rc-on-sand)'
|
||||
: 'border-accented bg-default text-highlighted'
|
||||
"
|
||||
@click="emit('select-none')"
|
||||
>
|
||||
<UIcon name="i-lucide-image-off" class="size-4 shrink-0" />
|
||||
<span class="font-medium">{{ t('No image') }}</span>
|
||||
<span class="flex-1" :class="noneChosen ? 'text-(--rc-on-sand)' : 'text-muted'">{{
|
||||
t('Leave this field empty.')
|
||||
}}</span>
|
||||
<UIcon v-if="noneChosen" name="i-lucide-check" class="size-4 shrink-0" />
|
||||
</button>
|
||||
<span v-if="!plain" data-testid="picker-summary" class="text-xs/5 text-muted">{{
|
||||
summary
|
||||
}}</span>
|
||||
@@ -268,7 +311,7 @@ onBeforeUnmount(clearDebounce)
|
||||
type="button"
|
||||
data-picker-tile
|
||||
:data-image-id="item.id"
|
||||
:aria-label="accessibleName(item)"
|
||||
:aria-label="tileName(item)"
|
||||
:aria-pressed="isSelected(item)"
|
||||
class="group flex min-w-0 flex-col gap-1.5 text-start focus-visible:outline-none"
|
||||
@click="emit('select', item)"
|
||||
@@ -287,6 +330,11 @@ onBeforeUnmount(clearDebounce)
|
||||
class="w-full border border-default"
|
||||
:class="tileAspect"
|
||||
/>
|
||||
<span
|
||||
v-if="isCurrent(item)"
|
||||
class="absolute bottom-1 left-1 inline-flex h-4.5 items-center bg-(--rc-sand) px-1.25 text-[0.6875rem] font-medium text-(--rc-on-sand)"
|
||||
>{{ t('Current') }}</span
|
||||
>
|
||||
<span
|
||||
v-if="isInArticle(item)"
|
||||
class="absolute bottom-1 left-1 inline-flex h-4.5 items-center bg-secondary-200 px-1.25 text-[0.6875rem] font-medium text-primary-900 dark:bg-secondary-300"
|
||||
@@ -338,7 +386,7 @@ onBeforeUnmount(clearDebounce)
|
||||
<p v-if="hint" class="text-xs/5 text-muted">{{ hint }}</p>
|
||||
|
||||
<div
|
||||
v-if="selected && !plain"
|
||||
v-if="selected && !plain && !noneChosen"
|
||||
data-testid="picker-panel"
|
||||
class="flex flex-col gap-3 border-t border-default pt-3"
|
||||
>
|
||||
@@ -355,12 +403,14 @@ onBeforeUnmount(clearDebounce)
|
||||
</div>
|
||||
</div>
|
||||
<VariantPickerField
|
||||
v-if="!field"
|
||||
v-model:variant="variant"
|
||||
:image="selected"
|
||||
:variants="variants ?? []"
|
||||
:ready="variantsReady ?? false"
|
||||
/>
|
||||
<UFormField
|
||||
v-if="!field"
|
||||
:label="t('Alt text (optional)')"
|
||||
:help="
|
||||
t(
|
||||
|
||||
Reference in new issue
Block a user