175 lines
4.5 KiB
Vue
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>
|