Files
RenewCMS/web/src/components/UserMenu.vue
T

175 lines
4.5 KiB
Vue

<script setup lang="ts">
import { locale, t } from '@/i18n'
import { computed, ref } from 'vue'
import { useMediaQuery } from '@vueuse/core'
import { colorModeOptions, useAccountMenu } from '@/composables/useAccountMenu'
import { chooseLanguage, languageOptions } from '@/composables/useLanguage'
import { useAccountMenuRadios } from '@/composables/useRadioMenuRoles'
const DESKTOP_QUERY = '(min-width: 64rem)'
const ITEM_CLASS = 'h-8 items-center gap-2.5 px-2 py-0 text-sm font-normal text-highlighted'
const CHOICE_CLASS = 'h-8 items-center gap-2.5 px-2 py-0 text-base font-normal text-highlighted'
const SECTION_LABEL_CLASS =
'px-2 pt-1.5 pb-0.5 text-[0.6875rem] font-medium uppercase tracking-[0.04em] text-muted'
const props = defineProps<{
username: string
displayName?: string
email?: string
collapsed: boolean
sheetOpen?: boolean
}>()
const emit = defineEmits<{
logout: []
'open-sheet': [chip: HTMLElement | undefined]
}>()
const { colorMode, name, initials, onProfile, goToProfile } = useAccountMenu(() => props)
const isDesktop = useMediaQuery(DESKTOP_QUERY)
const open = ref(false)
useAccountMenuRadios(open)
const chip = ref<{ $el: HTMLElement } | null>(null)
const chipActive = computed(() => open.value || onProfile.value)
const triggerAriaLabel = computed(() =>
props.collapsed ? t('{0} — account menu', [name.value]) : undefined,
)
const chipClass = computed(() => [
'h-10 gap-2.5 px-1.5 font-normal',
props.collapsed ? 'justify-center' : 'justify-start',
chipActive.value
? 'bg-secondary-200 text-primary-900 hover:bg-secondary-200 hover:text-primary-900 dark:bg-secondary-300 dark:hover:bg-secondary-300'
: 'text-highlighted',
])
function onUpdateOpen(value: boolean) {
if (isDesktop.value) {
open.value = value
return
}
if (value) {
emit('open-sheet', chip.value?.$el)
}
}
const chipExpanded = computed(() => (isDesktop.value ? open.value : props.sheetOpen === true))
const items = computed(() => [
[
{
type: 'label' as const,
label: name.value,
slot: 'account-header' as const,
class: 'mb-1 border-b border-default px-2 pt-2 pb-2.5',
},
{
get label() {
return t('Profile')
},
icon: 'i-lucide-circle-user',
class: onProfile.value ? `${ITEM_CLASS} bg-elevated` : ITEM_CLASS,
onSelect: goToProfile,
},
],
[
{
type: 'label' as const,
get label() {
return t('Theme')
},
class: SECTION_LABEL_CLASS,
},
...colorModeOptions.map((option) => ({
type: 'checkbox' as const,
label: option.label,
checked: colorMode.preference === option.value,
class: CHOICE_CLASS,
onSelect: (event: Event) => event.preventDefault(),
onUpdateChecked: (checked: boolean) => {
if (checked) {
colorMode.preference = option.value
}
},
})),
],
[
{
type: 'label' as const,
get label() {
return t('Language')
},
class: SECTION_LABEL_CLASS,
},
...languageOptions.map((option) => ({
type: 'checkbox' as const,
label: option.label,
checked: locale.value === option.value,
class: CHOICE_CLASS,
onSelect: (event: Event) => event.preventDefault(),
onUpdateChecked: () => chooseLanguage(option.value),
})),
],
[
{
get label() {
return t('Log out')
},
icon: 'i-lucide-log-out',
class: ITEM_CLASS,
onSelect: () => emit('logout'),
},
],
])
</script>
<template>
<UDropdownMenu
:open="open"
:items="items"
:content="{ side: 'top', align: 'start', sideOffset: 4, loop: true }"
:ui="{
content: 'w-56 p-0 ring-0 border border-default',
itemLeadingIcon: 'size-4 text-highlighted',
itemTrailingIcon: 'size-4',
}"
@update:open="onUpdateOpen"
>
<UButton
ref="chip"
color="neutral"
variant="ghost"
block
:title="t('Account')"
:aria-expanded="chipExpanded"
:class="chipClass"
:trailing-icon="collapsed ? undefined : 'i-lucide-chevrons-up-down'"
:ui="{ trailingIcon: 'ms-auto size-3.5 text-current' }"
:aria-label="triggerAriaLabel"
>
<template #leading>
<UAvatar
aria-hidden="true"
:text="initials"
size="sm"
:ui="{
root: 'size-6.5 bg-secondary-300',
fallback: 'text-[0.6875rem] font-semibold text-primary-900',
}"
/>
</template>
<span v-if="!collapsed" class="truncate font-medium">{{ name }}</span>
</UButton>
<template #account-header>
<span class="flex min-w-0 flex-col gap-0.5">
<span class="truncate text-sm font-semibold text-highlighted">{{ name }}</span>
<span class="truncate text-xs/5 font-normal text-muted">{{ email }}</span>
</span>
</template>
</UDropdownMenu>
</template>