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">
|
<script setup lang="ts">
|
||||||
import { dropdownContextKey } from './ctx'
|
import { dropdownContextKey } from './ctx'
|
||||||
|
defineProps<{
|
||||||
|
placement?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
const dropdown = $ref<any>()
|
const dropdown = $ref<any>()
|
||||||
|
|
||||||
|
@ -9,7 +12,7 @@ provide(dropdownContextKey, {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VDropdown v-bind="$attrs" ref="dropdown" :class="{ dark: isDark }">
|
<VDropdown v-bind="$attrs" ref="dropdown" :class="{ dark: isDark }" :placement="placement || 'auto'">
|
||||||
<slot />
|
<slot />
|
||||||
<template #popper="scope">
|
<template #popper="scope">
|
||||||
<slot name="popper" v-bind="scope" />
|
<slot name="popper" v-bind="scope" />
|
||||||
|
|
|
@ -1,30 +1,58 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
// only one icon can be lit up at the same time
|
||||||
|
const moreMenuVisible = ref(false)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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">
|
<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 />
|
<div i-ri:home-5-line />
|
||||||
</NuxtLink>
|
</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 />
|
<div i-ri:notification-4-line />
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</template>
|
</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 />
|
<div i-ri:hashtag />
|
||||||
</NuxtLink>
|
</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 />
|
<div i-ri:group-2-line />
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink to="/public" active-class="text-primary" flex flex-row items-center place-content-center h-full flex-1 @click="$scrollToTop">
|
<template v-if="!currentUser">
|
||||||
<div i-ri:earth-line />
|
<NuxtLink to="/public" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 @click="$scrollToTop">
|
||||||
</NuxtLink>
|
<div i-ri:earth-line />
|
||||||
|
</NuxtLink>
|
||||||
|
</template>
|
||||||
<template v-if="currentUser">
|
<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 />
|
<div i-ri:at-line />
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</template>
|
</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>
|
</nav>
|
||||||
</template>
|
</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()"
|
@click="toggleZenMode()"
|
||||||
/>
|
/>
|
||||||
</CommonTooltip>
|
</CommonTooltip>
|
||||||
<NavSelectLanguage />
|
<NavSelectLanguage>
|
||||||
<NavSelectFeatureFlags v-if="currentUser" />
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<button cursor-pointer hover:underline @click="openPreviewHelp">
|
<button cursor-pointer hover:underline @click="openPreviewHelp">
|
||||||
|
|
|
@ -3,13 +3,13 @@ const { notifications } = useNotifications()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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">
|
<template v-if="currentUser">
|
||||||
<NavSideItem :text="$t('nav_side.home')" to="/home" icon="i-ri:home-5-line" />
|
<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">
|
<NavSideItem :text="$t('nav_side.notifications')" to="/notifications" icon="i-ri:notification-4-line">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<div flex relative>
|
<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>
|
<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 : '•' }}
|
{{ notifications < 10 ? notifications : '•' }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -31,7 +31,7 @@ const { notifications } = useNotifications()
|
||||||
icon="i-ri:account-circle-line"
|
icon="i-ri:account-circle-line"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<AccountAvatar :account="currentUser.account" h="1.2em" />
|
<AccountAvatar :account="currentUser.account" h="1.2em" md:text-size-inherit text-xl />
|
||||||
</template>
|
</template>
|
||||||
<ContentRich
|
<ContentRich
|
||||||
:content="getDisplayName(currentUser.account, { rich: true }) || $t('nav_side.profile')"
|
:content="getDisplayName(currentUser.account, { rich: true }) || $t('nav_side.profile')"
|
||||||
|
|
|
@ -26,9 +26,9 @@ useCommand({
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NuxtLink :to="to" active-class="text-primary" group focus:outline-none @click="$scrollToTop">
|
<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">
|
<slot name="icon">
|
||||||
<div :class="icon" />
|
<div :class="icon" md:text-size-inherit text-xl />
|
||||||
</slot>
|
</slot>
|
||||||
<slot>
|
<slot>
|
||||||
<span>{{ text }}</span>
|
<span>{{ text }}</span>
|
||||||
|
|
|
@ -1,24 +1,17 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
const featureFlags = useFeatureFlags()
|
const featureFlags = useFeatureFlags()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<CommonTooltip :content="t('nav_footer.select_feature_flags')">
|
<CommonDropdown placement="top">
|
||||||
<CommonDropdown>
|
<slot />
|
||||||
<button flex :aria-label="t('nav_footer.select_feature_flags')">
|
<template #popper>
|
||||||
<div i-ri:flag-line text-lg />
|
<CommonDropdownItem
|
||||||
</button>
|
:checked="featureFlags.experimentalVirtualScroll"
|
||||||
|
@click="toggleFeatureFlag('experimentalVirtualScroll')"
|
||||||
<template #popper>
|
>
|
||||||
<CommonDropdownItem
|
{{ $t('feature_flag.virtual_scroll') }}
|
||||||
:checked="featureFlags.experimentalVirtualScroll"
|
</CommonDropdownItem>
|
||||||
@click="toggleFeatureFlag('experimentalVirtualScroll')"
|
</template>
|
||||||
>
|
</CommonDropdown>
|
||||||
{{ t('feature_flag.virtual_scroll') }}
|
|
||||||
</CommonDropdownItem>
|
|
||||||
</template>
|
|
||||||
</CommonDropdown>
|
|
||||||
</CommonTooltip>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -7,22 +7,18 @@ const { locales } = useI18n() as { locales: ComputedRef<LocaleObject[]> }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<CommonTooltip :content="t('nav_footer.select_language')">
|
<CommonDropdown>
|
||||||
<CommonDropdown>
|
<slot />
|
||||||
<button flex :aria-label="t('nav_footer.select_language')">
|
|
||||||
<div i-ri:earth-line text-lg />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<template #popper>
|
<template #popper>
|
||||||
<CommonDropdownItem
|
<CommonDropdownItem
|
||||||
v-for="item in locales"
|
v-for="item in locales"
|
||||||
:key="item.code"
|
:key="item.code"
|
||||||
:checked="item.code === locale"
|
:checked="item.code === locale"
|
||||||
@click="setLocale(item.code)"
|
@click="setLocale(item.code)"
|
||||||
>
|
>
|
||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
</CommonDropdownItem>
|
</CommonDropdownItem>
|
||||||
</template>
|
</template>
|
||||||
</CommonDropdown>
|
</CommonDropdown>
|
||||||
</CommonTooltip>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</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>
|
<div min-h-screen>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -81,6 +81,10 @@
|
||||||
"open_in_original_site": "Open in original site",
|
"open_in_original_site": "Open in original site",
|
||||||
"pin_on_profile": "Pin on profile",
|
"pin_on_profile": "Pin on profile",
|
||||||
"show_untranslated": "Show untranslated",
|
"show_untranslated": "Show untranslated",
|
||||||
|
"toggle_theme": {
|
||||||
|
"dark": "Toggle dark mode",
|
||||||
|
"light": "Toggle light mode"
|
||||||
|
},
|
||||||
"translate_post": "Translate post",
|
"translate_post": "Translate post",
|
||||||
"unblock_account": "Unblock {0}",
|
"unblock_account": "Unblock {0}",
|
||||||
"unblock_domain": "Unblock domain {0}",
|
"unblock_domain": "Unblock domain {0}",
|
||||||
|
|
|
@ -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": "切换深色模式",
|
||||||
|
"light": "切换亮色模式"
|
||||||
|
},
|
||||||
"translate_post": "翻译帖子",
|
"translate_post": "翻译帖子",
|
||||||
"unblock_account": "解除拉黑 {0}",
|
"unblock_account": "解除拉黑 {0}",
|
||||||
"unblock_domain": "解除拉黑域名 {0}",
|
"unblock_domain": "解除拉黑域名 {0}",
|
||||||
|
|
Loading…
Reference in a new issue