190 lines
4.7 KiB
Vue
190 lines
4.7 KiB
Vue
<script setup lang="ts">
|
|
import { locale, t } from '@/i18n'
|
|
|
|
import { ref, useId } from 'vue'
|
|
import type { ColorModePreference } from '@/composables/useColorMode'
|
|
import { colorModeOptions, useAccountMenu } from '@/composables/useAccountMenu'
|
|
import { chooseLanguage, languageOptions } from '@/composables/useLanguage'
|
|
|
|
const ITEM_SELECTOR = '[role="menuitem"], [role="menuitemradio"]'
|
|
const ITEM_CLASS = 'h-12 justify-start gap-3 px-4 text-[0.9375rem] font-normal text-highlighted'
|
|
const SECTION_LABEL_CLASS =
|
|
'px-4 pt-2 pb-0.5 text-[0.6875rem] font-medium uppercase tracking-[0.04em] text-muted'
|
|
|
|
const props = defineProps<{
|
|
username: string
|
|
displayName?: string
|
|
email?: string
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
logout: []
|
|
}>()
|
|
|
|
const open = defineModel<boolean>('open', { default: false })
|
|
|
|
const {
|
|
colorMode,
|
|
name,
|
|
initials,
|
|
onProfile,
|
|
goToProfile: navigateToProfile,
|
|
} = useAccountMenu(() => props)
|
|
const menu = ref<HTMLElement | null>(null)
|
|
const themeLabelId = useId()
|
|
const languageLabelId = useId()
|
|
|
|
function goToProfile() {
|
|
open.value = false
|
|
navigateToProfile()
|
|
}
|
|
|
|
function chooseColorMode(value: ColorModePreference) {
|
|
colorMode.preference = value
|
|
}
|
|
|
|
function logOut() {
|
|
open.value = false
|
|
emit('logout')
|
|
}
|
|
|
|
function moveFocus(event: KeyboardEvent) {
|
|
const items = Array.from(menu.value?.querySelectorAll<HTMLElement>(ITEM_SELECTOR) ?? [])
|
|
if (items.length === 0) {
|
|
return
|
|
}
|
|
const current = items.indexOf(document.activeElement as HTMLElement)
|
|
const lastIndex = items.length - 1
|
|
const targets: Record<string, number> = {
|
|
ArrowDown: current >= lastIndex ? 0 : current + 1,
|
|
ArrowUp: current <= 0 ? lastIndex : current - 1,
|
|
Home: 0,
|
|
End: lastIndex,
|
|
}
|
|
const target = targets[event.key]
|
|
if (target === undefined) {
|
|
return
|
|
}
|
|
event.preventDefault()
|
|
items[target]?.focus()
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<USlideover
|
|
v-model:open="open"
|
|
side="bottom"
|
|
:title="t('Account')"
|
|
:description="t('Your account menu')"
|
|
:close="false"
|
|
:ui="{
|
|
content: 'max-h-none',
|
|
body: 'p-0 sm:p-0',
|
|
header: 'sr-only',
|
|
}"
|
|
>
|
|
<template #body>
|
|
<div
|
|
ref="menu"
|
|
role="menu"
|
|
:aria-label="t('Account')"
|
|
class="pt-2 pb-6"
|
|
@keydown="moveFocus"
|
|
>
|
|
<div class="mx-auto mb-2 h-1 w-9 bg-accented" aria-hidden="true" />
|
|
<div class="mb-1 flex items-center gap-3 border-b border-default px-4 pt-2 pb-3.5">
|
|
<UAvatar
|
|
aria-hidden="true"
|
|
:text="initials"
|
|
:ui="{
|
|
root: 'size-9 bg-secondary-300',
|
|
fallback: 'text-[0.8125rem] pt-0.5 font-semibold text-primary-900',
|
|
}"
|
|
/>
|
|
<div class="flex min-w-0 flex-col">
|
|
<span class="truncate text-[0.9375rem] font-semibold text-highlighted">{{
|
|
name
|
|
}}</span>
|
|
<span class="truncate text-muted">{{ email }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<UButton
|
|
role="menuitem"
|
|
color="neutral"
|
|
variant="ghost"
|
|
block
|
|
icon="i-lucide-circle-user"
|
|
:class="[ITEM_CLASS, onProfile ? 'bg-elevated' : '']"
|
|
:ui="{ leadingIcon: 'size-4.5' }"
|
|
@click="goToProfile"
|
|
>
|
|
{{ t('Profile') }}
|
|
</UButton>
|
|
|
|
<USeparator class="my-1" />
|
|
|
|
<div role="group" :aria-labelledby="themeLabelId">
|
|
<div :id="themeLabelId" :class="SECTION_LABEL_CLASS">{{ t('Theme') }}</div>
|
|
<UButton
|
|
v-for="option in colorModeOptions"
|
|
:key="option.value"
|
|
role="menuitemradio"
|
|
color="neutral"
|
|
variant="ghost"
|
|
block
|
|
:aria-checked="colorMode.preference === option.value"
|
|
:trailing-icon="
|
|
colorMode.preference === option.value ? 'i-lucide-check' : undefined
|
|
"
|
|
:class="ITEM_CLASS"
|
|
:ui="{ trailingIcon: 'ms-auto size-4.5' }"
|
|
@click="chooseColorMode(option.value)"
|
|
>
|
|
{{ option.label }}
|
|
</UButton>
|
|
</div>
|
|
|
|
<USeparator class="my-1" />
|
|
|
|
<div role="group" :aria-labelledby="languageLabelId">
|
|
<div :id="languageLabelId" :class="SECTION_LABEL_CLASS">
|
|
{{ t('Language') }}
|
|
</div>
|
|
<UButton
|
|
v-for="option in languageOptions"
|
|
:key="option.value"
|
|
role="menuitemradio"
|
|
color="neutral"
|
|
variant="ghost"
|
|
block
|
|
:lang="option.value"
|
|
:aria-checked="locale === option.value"
|
|
:trailing-icon="locale === option.value ? 'i-lucide-check' : undefined"
|
|
:class="ITEM_CLASS"
|
|
:ui="{ trailingIcon: 'ms-auto size-4.5' }"
|
|
@click="chooseLanguage(option.value)"
|
|
>
|
|
{{ option.label }}
|
|
</UButton>
|
|
</div>
|
|
|
|
<USeparator class="my-1" />
|
|
|
|
<UButton
|
|
role="menuitem"
|
|
color="neutral"
|
|
variant="ghost"
|
|
block
|
|
icon="i-lucide-log-out"
|
|
:class="ITEM_CLASS"
|
|
:ui="{ leadingIcon: 'size-4.5' }"
|
|
@click="logOut"
|
|
>
|
|
{{ t('Log out') }}
|
|
</UButton>
|
|
</div>
|
|
</template>
|
|
</USlideover>
|
|
</template>
|