forked from Mirrors/elk
feat: add nav more menu on mobile (#322)
Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
This commit is contained in:
parent
8f32b1ce22
commit
cbd5867275
11 changed files with 237 additions and 54 deletions
|
@ -1,5 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import { dropdownContextKey } from './ctx'
|
||||
defineProps<{
|
||||
placement?: string
|
||||
}>()
|
||||
|
||||
const dropdown = $ref<any>()
|
||||
|
||||
|
@ -9,7 +12,7 @@ provide(dropdownContextKey, {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<VDropdown v-bind="$attrs" ref="dropdown" :class="{ dark: isDark }">
|
||||
<VDropdown v-bind="$attrs" ref="dropdown" :class="{ dark: isDark }" :placement="placement || 'auto'">
|
||||
<slot />
|
||||
<template #popper="scope">
|
||||
<slot name="popper" v-bind="scope" />
|
||||
|
|
|
@ -1,30 +1,58 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
// only one icon can be lit up at the same time
|
||||
const moreMenuVisible = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav h-14 border="t base" flex flex-row>
|
||||
<nav
|
||||
h-14 border="t base" flex flex-row text-xl
|
||||
of-y-scroll overscroll-none
|
||||
class="scrollbar-hide after-content-empty after:(h-[calc(100%+0.5px)] w-0.1px pointer-events-none)"
|
||||
>
|
||||
<!-- These weird styles above are used for scroll locking, don't change it unless you know exactly what you're doing. -->
|
||||
<template v-if="currentUser">
|
||||
<NuxtLink to="/home" active-class="text-primary" flex flex-row items-center place-content-center h-full flex-1 @click="$scrollToTop">
|
||||
<NuxtLink to="/home" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 @click="$scrollToTop">
|
||||
<div i-ri:home-5-line />
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/notifications" active-class="text-primary" flex flex-row items-center place-content-center h-full flex-1 @click="$scrollToTop">
|
||||
<NuxtLink to="/notifications" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 @click="$scrollToTop">
|
||||
<div i-ri:notification-4-line />
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<NuxtLink to="/explore" active-class="text-primary" flex flex-row items-center place-content-center h-full flex-1 @click="$scrollToTop">
|
||||
<NuxtLink to="/explore" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 @click="$scrollToTop">
|
||||
<div i-ri:hashtag />
|
||||
</NuxtLink>
|
||||
<NuxtLink group to="/public/local" active-class="text-primary" flex flex-row items-center place-content-center h-full flex-1 @click="$scrollToTop">
|
||||
<NuxtLink group to="/public/local" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 @click="$scrollToTop">
|
||||
<div i-ri:group-2-line />
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/public" active-class="text-primary" flex flex-row items-center place-content-center h-full flex-1 @click="$scrollToTop">
|
||||
<div i-ri:earth-line />
|
||||
</NuxtLink>
|
||||
<template v-if="!currentUser">
|
||||
<NuxtLink to="/public" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 @click="$scrollToTop">
|
||||
<div i-ri:earth-line />
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<template v-if="currentUser">
|
||||
<NuxtLink to="/conversations" active-class="text-primary" flex flex-row items-center place-content-center h-full flex-1 @click="$scrollToTop">
|
||||
<NuxtLink to="/conversations" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 @click="$scrollToTop">
|
||||
<div i-ri:at-line />
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<NavBottomMoreMenu v-slot="{ changeShow, show }" v-model="moreMenuVisible" flex flex-row items-center place-content-center h-full flex-1 cursor-pointer>
|
||||
<label
|
||||
flex items-center place-content-center h-full flex-1 class="selete-none"
|
||||
:class="show ? '!text-primary' : ''"
|
||||
>
|
||||
<input type="checkbox" z="-1" absolute inset-0 opacity-0 @click="changeShow">
|
||||
<span v-show="show" i-ri:close-fill />
|
||||
<span v-show="!show" i-ri:more-fill />
|
||||
</label>
|
||||
</NavBottomMoreMenu>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.scrollbar-hide {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
|
143
components/nav/NavBottomMoreMenu.vue
Normal file
143
components/nav/NavBottomMoreMenu.vue
Normal file
|
@ -0,0 +1,143 @@
|
|||
<script lang="ts" setup>
|
||||
import type { ComputedRef } from 'vue'
|
||||
import type { LocaleObject } from '#i18n'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue?: boolean
|
||||
}>()
|
||||
const emits = defineEmits<{
|
||||
(event: 'update:modelValue', value: boolean): void
|
||||
}>()
|
||||
const visible = useVModel(props, 'modelValue', emits, { passive: true })
|
||||
|
||||
const { t, locale, setLocale } = useI18n()
|
||||
const { locales } = useI18n() as { locales: ComputedRef<LocaleObject[]> }
|
||||
|
||||
const toggleLocales = () => {
|
||||
const codes = locales.value.map(item => item.code)
|
||||
setLocale(codes[(codes.indexOf(locale.value) + 1) % codes.length])
|
||||
}
|
||||
|
||||
function changeShow() {
|
||||
visible.value = !visible.value
|
||||
}
|
||||
|
||||
const buttonEl = ref<HTMLDivElement>()
|
||||
/** Close the drop-down menu if the mouse click is not on the drop-down menu button when the drop-down menu is opened */
|
||||
function clickEvent(mouse: MouseEvent) {
|
||||
if (mouse.target && !buttonEl.value?.children[0].contains(mouse.target as any)) {
|
||||
if (visible.value) {
|
||||
document.removeEventListener('click', clickEvent)
|
||||
visible.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch(visible, (val, oldVal) => {
|
||||
if (val && val !== oldVal) {
|
||||
if (!import.meta.env.SSR && typeof document !== 'undefined')
|
||||
document.addEventListener('click', clickEvent)
|
||||
}
|
||||
}, { flush: 'post' })
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (!import.meta.env.SSR)
|
||||
document.removeEventListener('click', clickEvent)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="buttonEl" flex items-center static>
|
||||
<slot :change-show="changeShow" :show="visible" />
|
||||
|
||||
<!-- Drawer -->
|
||||
<Transition
|
||||
enter-active-class="transition duration-250 ease-out children:(transition duration-250 ease-out)"
|
||||
enter-from-class="opacity-0 children:(transform translate-y-full)"
|
||||
enter-to-class="opacity-100 children:(transform translate-y-0)"
|
||||
leave-active-class="transition duration-250 ease-in children:(transition duration-250 ease-in)"
|
||||
leave-from-class="opacity-100 children:(transform translate-y-0)"
|
||||
leave-to-class="opacity-0 children:(transform translate-y-full)"
|
||||
persisted
|
||||
>
|
||||
<div
|
||||
v-show="visible"
|
||||
class="scrollbar-hide"
|
||||
absolute inset-x-0 top-auto bottom-full z-20 h-100vh
|
||||
flex items-end of-y-scroll of-x-hidden overscroll-none
|
||||
bg="black/50"
|
||||
>
|
||||
<!-- The style `scrollbar-hide overscroll-none overflow-y-scroll mb="-1px"` and `h="[calc(100%+0.5px)]"` is used to implement scroll locking, -->
|
||||
<!-- corresponding to issue: #106, so please don't remove it. -->
|
||||
<div absolute inset-0 opacity-0 h="[calc(100vh+0.5px)]" />
|
||||
<div
|
||||
class="scrollbar-hide"
|
||||
flex-1 min-w-48 py-6 mb="-1px"
|
||||
overflow-y-auto overscroll-none max-h="[calc(100vh-200px)]"
|
||||
rounded-t-lg bg="white/85 dark:neutral-900/85" backdrop-filter backdrop-blur-md
|
||||
border-t-1 border-base
|
||||
>
|
||||
<!-- Nav -->
|
||||
<NavSide />
|
||||
|
||||
<!-- Divider line -->
|
||||
<div border="neutral-300 dark:neutral-700 t-1" m="x-3 y-2" />
|
||||
|
||||
<!-- Function menu -->
|
||||
<div flex="~ col gap2">
|
||||
<!-- Toggle Theme -->
|
||||
<button
|
||||
flex flex-row items-center
|
||||
block px-5 py-2 focus-blue w-full
|
||||
text-sm text-base capitalize text-left whitespace-nowrap
|
||||
transition-colors duration-200 transform
|
||||
hover="bg-gray-100 dark:(bg-gray-700 text-white)"
|
||||
@click="toggleDark()"
|
||||
>
|
||||
<span class="i-ri:sun-line dark:i-ri:moon-line flex-shrink-0 text-xl mr-4 !align-middle" />
|
||||
{{ !isDark ? t('menu.toggle_theme.dark') : t('menu.toggle_theme.light') }}
|
||||
</button>
|
||||
<!-- Switch languages -->
|
||||
<NavSelectLanguage>
|
||||
<button
|
||||
flex flex-row items-center
|
||||
block px-5 py-2 focus-blue w-full
|
||||
text-sm text-base capitalize text-left whitespace-nowrap
|
||||
transition-colors duration-200 transform
|
||||
hover="bg-gray-100 dark:(bg-gray-700 text-white)"
|
||||
@click.stop
|
||||
>
|
||||
<span class="i-ri:earth-line flex-shrink-0 text-xl mr-4 !align-middle" />
|
||||
{{ $t('nav_footer.select_language') }}
|
||||
</button>
|
||||
</NavSelectLanguage>
|
||||
<!-- Toggle Feature Flags -->
|
||||
<NavSelectFeatureFlags v-if="currentUser">
|
||||
<button
|
||||
flex flex-row items-center
|
||||
block px-5 py-2 focus-blue w-full
|
||||
text-sm text-base capitalize text-left whitespace-nowrap
|
||||
transition-colors duration-200 transform
|
||||
hover="bg-gray-100 dark:(bg-gray-700 text-white)"
|
||||
@click.stop
|
||||
>
|
||||
<span class="i-ri:flag-line flex-shrink-0 text-xl mr-4 !align-middle" />
|
||||
{{ $t('nav_footer.select_feature_flags') }}
|
||||
</button>
|
||||
</NavSelectFeatureFlags>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.scrollbar-hide {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
|
@ -22,8 +22,20 @@ const buildTimeAgo = useTimeAgo(buildTime, timeAgoOptions)
|
|||
@click="toggleZenMode()"
|
||||
/>
|
||||
</CommonTooltip>
|
||||
<NavSelectLanguage />
|
||||
<NavSelectFeatureFlags v-if="currentUser" />
|
||||
<NavSelectLanguage>
|
||||
<CommonTooltip :content="$t('nav_footer.select_language')">
|
||||
<button flex :aria-label="$t('nav_footer.select_language')">
|
||||
<div i-ri:earth-line text-lg />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
</NavSelectLanguage>
|
||||
<NavSelectFeatureFlags v-if="currentUser">
|
||||
<CommonTooltip :content="$t('nav_footer.select_feature_flags')">
|
||||
<button flex :aria-label="$t('nav_footer.select_feature_flags')">
|
||||
<div i-ri:flag-line text-lg />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
</NavSelectFeatureFlags>
|
||||
</div>
|
||||
<div>
|
||||
<button cursor-pointer hover:underline @click="openPreviewHelp">
|
||||
|
|
|
@ -3,13 +3,13 @@ const { notifications } = useNotifications()
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<nav px3 py4 flex="~ col gap2" text-lg>
|
||||
<nav md:px3 md:py4 flex="~ col gap2" text-size-base leading-normal md:text-lg>
|
||||
<template v-if="currentUser">
|
||||
<NavSideItem :text="$t('nav_side.home')" to="/home" icon="i-ri:home-5-line" />
|
||||
<NavSideItem :text="$t('nav_side.notifications')" to="/notifications" icon="i-ri:notification-4-line">
|
||||
<template #icon>
|
||||
<div flex relative>
|
||||
<div class="i-ri:notification-4-line" />
|
||||
<div class="i-ri:notification-4-line" md:text-size-inherit text-xl />
|
||||
<div v-if="notifications" class="top-[-0.3rem] right-[-0.3rem]" absolute font-bold rounded-full h-4 w-4 text-xs bg-primary text-inverted flex items-center justify-center>
|
||||
{{ notifications < 10 ? notifications : '•' }}
|
||||
</div>
|
||||
|
@ -31,7 +31,7 @@ const { notifications } = useNotifications()
|
|||
icon="i-ri:account-circle-line"
|
||||
>
|
||||
<template #icon>
|
||||
<AccountAvatar :account="currentUser.account" h="1.2em" />
|
||||
<AccountAvatar :account="currentUser.account" h="1.2em" md:text-size-inherit text-xl />
|
||||
</template>
|
||||
<ContentRich
|
||||
:content="getDisplayName(currentUser.account, { rich: true }) || $t('nav_side.profile')"
|
||||
|
|
|
@ -26,9 +26,9 @@ useCommand({
|
|||
|
||||
<template>
|
||||
<NuxtLink :to="to" active-class="text-primary" group focus:outline-none @click="$scrollToTop">
|
||||
<div flex w-fit px5 py2 gap2 items-center transition-100 rounded-full group-hover:bg-active group-focus-visible:ring="2 current">
|
||||
<div flex w-fit px5 py2 md:gap2 gap4 items-center transition-100 rounded-full group-hover:bg-active group-focus-visible:ring="2 current">
|
||||
<slot name="icon">
|
||||
<div :class="icon" />
|
||||
<div :class="icon" md:text-size-inherit text-xl />
|
||||
</slot>
|
||||
<slot>
|
||||
<span>{{ text }}</span>
|
||||
|
|
|
@ -1,24 +1,17 @@
|
|||
<script setup lang="ts">
|
||||
const { t } = useI18n()
|
||||
|
||||
const featureFlags = useFeatureFlags()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CommonTooltip :content="t('nav_footer.select_feature_flags')">
|
||||
<CommonDropdown>
|
||||
<button flex :aria-label="t('nav_footer.select_feature_flags')">
|
||||
<div i-ri:flag-line text-lg />
|
||||
</button>
|
||||
|
||||
<template #popper>
|
||||
<CommonDropdownItem
|
||||
:checked="featureFlags.experimentalVirtualScroll"
|
||||
@click="toggleFeatureFlag('experimentalVirtualScroll')"
|
||||
>
|
||||
{{ t('feature_flag.virtual_scroll') }}
|
||||
</CommonDropdownItem>
|
||||
</template>
|
||||
</CommonDropdown>
|
||||
</CommonTooltip>
|
||||
<CommonDropdown placement="top">
|
||||
<slot />
|
||||
<template #popper>
|
||||
<CommonDropdownItem
|
||||
:checked="featureFlags.experimentalVirtualScroll"
|
||||
@click="toggleFeatureFlag('experimentalVirtualScroll')"
|
||||
>
|
||||
{{ $t('feature_flag.virtual_scroll') }}
|
||||
</CommonDropdownItem>
|
||||
</template>
|
||||
</CommonDropdown>
|
||||
</template>
|
||||
|
|
|
@ -7,22 +7,18 @@ const { locales } = useI18n() as { locales: ComputedRef<LocaleObject[]> }
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<CommonTooltip :content="t('nav_footer.select_language')">
|
||||
<CommonDropdown>
|
||||
<button flex :aria-label="t('nav_footer.select_language')">
|
||||
<div i-ri:earth-line text-lg />
|
||||
</button>
|
||||
<CommonDropdown>
|
||||
<slot />
|
||||
|
||||
<template #popper>
|
||||
<CommonDropdownItem
|
||||
v-for="item in locales"
|
||||
:key="item.code"
|
||||
:checked="item.code === locale"
|
||||
@click="setLocale(item.code)"
|
||||
>
|
||||
{{ item.name }}
|
||||
</CommonDropdownItem>
|
||||
</template>
|
||||
</CommonDropdown>
|
||||
</CommonTooltip>
|
||||
<template #popper>
|
||||
<CommonDropdownItem
|
||||
v-for="item in locales"
|
||||
:key="item.code"
|
||||
:checked="item.code === locale"
|
||||
@click="setLocale(item.code)"
|
||||
>
|
||||
{{ item.name }}
|
||||
</CommonDropdownItem>
|
||||
</template>
|
||||
</CommonDropdown>
|
||||
</template>
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
</slot>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="w-full mb14 md:(w-3/4 mb0) lg:(w-2/4 mb0) min-h-screen" border="l r base">
|
||||
<div class="w-full mb14 md:(w-3/4 mb0) lg:(w-2/4 mb0) min-h-screen" border="none md:l md:r base">
|
||||
<div min-h-screen>
|
||||
<slot />
|
||||
</div>
|
||||
|
|
|
@ -81,6 +81,10 @@
|
|||
"open_in_original_site": "Open in original site",
|
||||
"pin_on_profile": "Pin on profile",
|
||||
"show_untranslated": "Show untranslated",
|
||||
"toggle_theme": {
|
||||
"dark": "Toggle dark mode",
|
||||
"light": "Toggle light mode"
|
||||
},
|
||||
"translate_post": "Translate post",
|
||||
"unblock_account": "Unblock {0}",
|
||||
"unblock_domain": "Unblock domain {0}",
|
||||
|
|
|
@ -81,6 +81,10 @@
|
|||
"open_in_original_site": "从源站打开",
|
||||
"pin_on_profile": "钉选在个人资料上",
|
||||
"show_untranslated": "显示原文",
|
||||
"toggle_theme": {
|
||||
"dark": "切换深色模式",
|
||||
"light": "切换亮色模式"
|
||||
},
|
||||
"translate_post": "翻译帖子",
|
||||
"unblock_account": "解除拉黑 {0}",
|
||||
"unblock_domain": "解除拉黑域名 {0}",
|
||||
|
|
Loading…
Reference in a new issue