feat(SPA): screens for Sites and Authors

This commit is contained in:
Florian Sylvain committed 2026-10-14 23:00:50 +02:00
1 parent b34a173d83
commit 3d81d2c717
82 files changed
+10676 -37

No files matched your search

+115
View File
@@ -0,0 +1,115 @@
<script setup lang="ts">
import { t } from '@/i18n'
import { computed } from 'vue'
import AuthorAvatar from '@/components/AuthorAvatar.vue'
import type { LinkableUser } from '@/lib/directoryTypes'
const props = withDefaults(
defineProps<{
users: LinkableUser[]
status: 'loading' | 'ready' | 'error'
idlePlaceholder?: string
clearable?: boolean
disabled?: boolean
invalid?: boolean
}>(),
{ idlePlaceholder: '', clearable: false, disabled: false, invalid: false },
)
const emit = defineEmits<{ retry: []; open: [] }>()
const menuOpen = defineModel<boolean>('open', { default: false })
const userId = defineModel<number | null>({ default: null })
const items = computed(() =>
props.users.length === 0
? []
: [
{ type: 'label' as const, label: t('Users without an author') },
...props.users.map((user) => ({
label: user.label,
description: user.email,
value: user.id,
avatarName: user.label,
})),
],
)
const placeholder = computed(() =>
menuOpen.value || !props.idlePlaceholder ? t('Choose a user') : props.idlePlaceholder,
)
const chosen = computed(() => props.users.find((user) => user.id === userId.value))
function onOpen(value: boolean) {
if (value) {
emit('open')
}
}
</script>
<template>
<USelectMenu
v-model="userId"
v-model:open="menuOpen"
:portal="false"
:content="{ position: 'inline' }"
:items="items"
value-key="value"
:search-input="false"
:placeholder="placeholder"
:loading="status === 'loading'"
:disabled="disabled"
:color="invalid ? 'error' : undefined"
:highlight="invalid"
:clear="clearable && userId !== null"
class="w-full"
:ui="{
base: 'h-11 rounded-none text-lg ring-(--ui-border-accented) md:h-10 md:text-sm',
trailingIcon: 'size-3.5 text-muted',
content: 'rounded-none shadow-(--shadow-popover)',
label: 'px-2 py-1.5 text-xs text-muted',
item: 'min-h-12 gap-2 px-2 py-1 data-[state=checked]:bg-elevated md:min-h-9',
itemDescription: 'truncate text-xs text-muted',
placeholder: 'text-muted',
}"
@update:open="onOpen"
>
<template #default>
<span v-if="chosen" class="truncate text-highlighted">{{ chosen.label }}</span>
<span v-else class="truncate text-muted">{{ placeholder }}</span>
</template>
<template #item-leading="{ item }">
<AuthorAvatar v-if="'avatarName' in item" :name="String(item.avatarName)" :size="24" />
</template>
<template #empty>
<div v-if="status === 'error'" class="flex flex-col items-start gap-2 p-3">
<span class="text-error">{{ t("Couldn't load users") }}</span>
<UButton
color="neutral"
variant="outline"
icon="i-lucide-rotate-cw"
class="h-11 gap-1.5 border border-accented bg-default px-2.5 text-xs text-highlighted ring-0 md:h-7"
:ui="{ leadingIcon: 'size-3.5' }"
@click="emit('retry')"
>
{{ t('Try again') }}
</UButton>
</div>
<div
v-else-if="status === 'loading'"
class="flex flex-col gap-2.5 p-3"
aria-busy="true"
>
<USkeleton
v-for="n in 3"
:key="n"
class="h-3 rounded-none bg-accented"
:class="n === 3 ? 'w-3/5' : 'w-4/5'"
/>
</div>
<div v-else class="p-3 text-muted">{{ t('Every user already has an author.') }}</div>
</template>
</USelectMenu>
</template>