refactor: migrate to nuxt compatibilityVersion: 4 (#3298)
This commit is contained in:
parent
46e4433e1c
commit
a3fbc056a9
342 changed files with 1200 additions and 2932 deletions
64
app/components/nav/NavBottom.vue
Normal file
64
app/components/nav/NavBottom.vue
Normal file
|
@ -0,0 +1,64 @@
|
|||
<script setup lang="ts">
|
||||
import type { Component } from 'vue'
|
||||
import type { NavButtonName } from '../../composables/settings'
|
||||
|
||||
import {
|
||||
NavButtonBookmark,
|
||||
NavButtonCompose,
|
||||
NavButtonExplore,
|
||||
NavButtonFavorite,
|
||||
NavButtonFederated,
|
||||
NavButtonHashtag,
|
||||
NavButtonHome,
|
||||
NavButtonList,
|
||||
NavButtonLocal,
|
||||
NavButtonMention,
|
||||
NavButtonMoreMenu,
|
||||
NavButtonNotification,
|
||||
NavButtonSearch,
|
||||
} from '#components'
|
||||
|
||||
import { STORAGE_KEY_BOTTOM_NAV_BUTTONS } from '~/constants'
|
||||
|
||||
interface NavButton {
|
||||
name: string
|
||||
component: Component
|
||||
}
|
||||
|
||||
const navButtons: NavButton[] = [
|
||||
{ name: 'home', component: NavButtonHome },
|
||||
{ name: 'search', component: NavButtonSearch },
|
||||
{ name: 'notification', component: NavButtonNotification },
|
||||
{ name: 'mention', component: NavButtonMention },
|
||||
{ name: 'favorite', component: NavButtonFavorite },
|
||||
{ name: 'bookmark', component: NavButtonBookmark },
|
||||
{ name: 'compose', component: NavButtonCompose },
|
||||
{ name: 'explore', component: NavButtonExplore },
|
||||
{ name: 'local', component: NavButtonLocal },
|
||||
{ name: 'federated', component: NavButtonFederated },
|
||||
{ name: 'list', component: NavButtonList },
|
||||
{ name: 'hashtag', component: NavButtonHashtag },
|
||||
{ name: 'moreMenu', component: NavButtonMoreMenu },
|
||||
]
|
||||
|
||||
const defaultSelectedNavButtonNames: NavButtonName[] = currentUser.value
|
||||
? ['home', 'search', 'notification', 'mention', 'moreMenu']
|
||||
: ['explore', 'local', 'federated', 'moreMenu']
|
||||
const selectedNavButtonNames = useLocalStorage<NavButtonName[]>(STORAGE_KEY_BOTTOM_NAV_BUTTONS, defaultSelectedNavButtonNames)
|
||||
|
||||
const selectedNavButtons = computed(() => selectedNavButtonNames.value.map(name => navButtons.find(navButton => navButton.name === name)))
|
||||
|
||||
// only one icon can be lit up at the same time
|
||||
const moreMenuVisible = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- This weird styles above are used for scroll locking, don't change it unless you know exactly what you're doing. -->
|
||||
<nav
|
||||
h-14 border="t base" flex flex-row text-xl
|
||||
of-y-scroll scrollbar-hide overscroll-none
|
||||
class="after-content-empty after:(h-[calc(100%+0.5px)] w-0.1px pointer-events-none)"
|
||||
>
|
||||
<Component :is="navButton!.component" v-for="navButton in selectedNavButtons" :key="navButton!.name" :active-class="moreMenuVisible ? '' : 'text-primary'" />
|
||||
</nav>
|
||||
</template>
|
194
app/components/nav/NavBottomMoreMenu.vue
Normal file
194
app/components/nav/NavBottomMoreMenu.vue
Normal file
|
@ -0,0 +1,194 @@
|
|||
<script setup lang="ts">
|
||||
import { invoke } from '@vueuse/core'
|
||||
|
||||
const modelValue = defineModel<boolean>({ required: true })
|
||||
const colorMode = useColorMode()
|
||||
|
||||
const userSettings = useUserSettings()
|
||||
|
||||
const drawerEl = ref<HTMLDivElement>()
|
||||
|
||||
function toggleVisible() {
|
||||
modelValue.value = !modelValue.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
|
||||
* @param mouse
|
||||
*/
|
||||
function clickEvent(mouse: MouseEvent) {
|
||||
if (mouse.target && !buttonEl.value?.children[0].contains(mouse.target as any)) {
|
||||
if (modelValue.value) {
|
||||
document.removeEventListener('click', clickEvent)
|
||||
modelValue.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleDark() {
|
||||
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
|
||||
}
|
||||
|
||||
watch(modelValue, (val) => {
|
||||
if (val && typeof document !== 'undefined')
|
||||
document.addEventListener('click', clickEvent)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', clickEvent)
|
||||
})
|
||||
|
||||
// Pull down to close
|
||||
const { dragging, dragDistance } = invoke(() => {
|
||||
const triggerDistance = 120
|
||||
|
||||
let scrollTop = 0
|
||||
let beforeTouchPointY = 0
|
||||
|
||||
const dragDistance = ref(0)
|
||||
const dragging = ref(false)
|
||||
|
||||
useEventListener(drawerEl, 'scroll', (e: Event) => {
|
||||
scrollTop = (e.target as HTMLDivElement).scrollTop
|
||||
|
||||
// Prevent the page from scrolling when the drawer is being dragged.
|
||||
if (dragDistance.value > 0)
|
||||
(e.target as HTMLDivElement).scrollTop = 0
|
||||
}, { passive: true })
|
||||
|
||||
useEventListener(drawerEl, 'touchstart', (e: TouchEvent) => {
|
||||
if (!modelValue.value)
|
||||
return
|
||||
|
||||
beforeTouchPointY = e.touches[0].pageY
|
||||
dragDistance.value = 0
|
||||
}, { passive: true })
|
||||
|
||||
useEventListener(drawerEl, 'touchmove', (e: TouchEvent) => {
|
||||
if (!modelValue.value)
|
||||
return
|
||||
|
||||
// Do not move the entire drawer when its contents are not scrolled to the top.
|
||||
if (scrollTop > 0 && dragDistance.value <= 0) {
|
||||
dragging.value = false
|
||||
beforeTouchPointY = e.touches[0].pageY
|
||||
return
|
||||
}
|
||||
|
||||
const { pageY } = e.touches[0]
|
||||
|
||||
// Calculate the drag distance.
|
||||
dragDistance.value += pageY - beforeTouchPointY
|
||||
if (dragDistance.value < 0)
|
||||
dragDistance.value = 0
|
||||
beforeTouchPointY = pageY
|
||||
|
||||
// Marked as dragging.
|
||||
if (dragDistance.value > 1)
|
||||
dragging.value = true
|
||||
|
||||
// Prevent the page from scrolling when the drawer is being dragged.
|
||||
if (dragDistance.value > 0) {
|
||||
if (e?.cancelable && e?.preventDefault)
|
||||
e.preventDefault()
|
||||
e?.stopPropagation()
|
||||
}
|
||||
}, { passive: true })
|
||||
|
||||
useEventListener(drawerEl, 'touchend', () => {
|
||||
if (!modelValue.value)
|
||||
return
|
||||
|
||||
if (dragDistance.value >= triggerDistance)
|
||||
modelValue.value = false
|
||||
|
||||
dragging.value = false
|
||||
// code
|
||||
}, { passive: true })
|
||||
|
||||
return {
|
||||
dragDistance,
|
||||
dragging,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="buttonEl" flex items-center static>
|
||||
<slot :toggle-visible="toggleVisible" :show="modelValue" />
|
||||
|
||||
<!-- Drawer -->
|
||||
<Transition
|
||||
enter-active-class="transition duration-250 ease-out"
|
||||
enter-from-class="opacity-0 children:(translate-y-full)"
|
||||
enter-to-class="opacity-100 children:(translate-y-0)"
|
||||
leave-active-class="transition duration-250 ease-in"
|
||||
leave-from-class="opacity-100 children:(translate-y-0)"
|
||||
leave-to-class="opacity-0 children:(translate-y-full)"
|
||||
>
|
||||
<div
|
||||
v-show="modelValue"
|
||||
absolute inset-x-0 top-auto bottom-full z-20 h-100vh
|
||||
flex items-end of-y-scroll of-x-hidden scrollbar-hide 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
|
||||
ref="drawerEl"
|
||||
:style="{
|
||||
transform: dragging ? `translateY(${dragDistance}px)` : '',
|
||||
}"
|
||||
:class="{
|
||||
'duration-0': dragging,
|
||||
'duration-250': !dragging,
|
||||
'backdrop-blur-md': !getPreferences(userSettings, 'optimizeForLowPerformanceDevice'),
|
||||
}"
|
||||
transition="transform ease-in"
|
||||
flex-1 min-w-48 py-6 mb="-1px"
|
||||
of-y-auto scrollbar-hide overscroll-none max-h="[calc(100vh-200px)]"
|
||||
rounded-t-lg bg="white/85 dark:neutral-900/85" backdrop-filter
|
||||
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 me-4 !align-middle" />
|
||||
{{ colorMode.value === 'light' ? $t('menu.toggle_theme.dark') : $t('menu.toggle_theme.light') }}
|
||||
</button>
|
||||
|
||||
<!-- Zen Mode -->
|
||||
<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)"
|
||||
:aria-label="$t('nav.zen_mode')"
|
||||
@click="togglePreferences('zenMode')"
|
||||
>
|
||||
<span :class="getPreferences(userSettings, 'zenMode') ? 'i-ri:layout-right-2-line' : 'i-ri:layout-right-line'" class="flex-shrink-0 text-xl me-4 !align-middle" />
|
||||
{{ $t('nav.zen_mode') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
99
app/components/nav/NavFooter.vue
Normal file
99
app/components/nav/NavFooter.vue
Normal file
|
@ -0,0 +1,99 @@
|
|||
<script setup lang="ts">
|
||||
const buildInfo = useBuildInfo()
|
||||
const timeAgoOptions = useTimeAgoOptions()
|
||||
const config = useRuntimeConfig()
|
||||
const userSettings = useUserSettings()
|
||||
|
||||
const buildTimeDate = new Date(buildInfo.time)
|
||||
const buildTimeAgo = useTimeAgo(buildTimeDate, timeAgoOptions)
|
||||
|
||||
const colorMode = useColorMode()
|
||||
function toggleDark() {
|
||||
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer p4 text-sm text-secondary-light flex="~ col">
|
||||
<div flex="~ gap2" items-center mb4>
|
||||
<CommonTooltip :content="$t('nav.toggle_theme')">
|
||||
<button flex i-ri:sun-line dark-i-ri:moon-line text-lg :aria-label="$t('nav.toggle_theme')" @click="toggleDark()" />
|
||||
</CommonTooltip>
|
||||
<CommonTooltip :content="$t('nav.zen_mode')">
|
||||
<button
|
||||
flex
|
||||
text-lg
|
||||
:class="getPreferences(userSettings, 'zenMode') ? 'i-ri:layout-right-2-line' : 'i-ri:layout-right-line'"
|
||||
:aria-label="$t('nav.zen_mode')"
|
||||
@click="togglePreferences('zenMode')"
|
||||
/>
|
||||
</CommonTooltip>
|
||||
<CommonTooltip :content="$t('magic_keys.dialog_header')">
|
||||
<button flex i-ri:keyboard-box-line dark-i-ri:keyboard-box-line text-lg :aria-label="$t('magic_keys.dialog_header')" @click="toggleKeyboardShortcuts" />
|
||||
</CommonTooltip>
|
||||
<CommonTooltip :content="$t('settings.about.sponsor_action')">
|
||||
<NuxtLink
|
||||
flex
|
||||
text-lg
|
||||
i-ri-heart-3-line hover="i-ri-heart-3-fill text-rose"
|
||||
:aria-label="$t('settings.about.sponsor_action')"
|
||||
href="https://github.com/sponsors/elk-zone"
|
||||
target="_blank"
|
||||
/>
|
||||
</CommonTooltip>
|
||||
</div>
|
||||
<div>
|
||||
<i18n-t v-if="isHydrated" keypath="nav.built_at">
|
||||
<time :datetime="String(buildTimeDate)" :title="$d(buildTimeDate, 'long')">{{ buildTimeAgo }}</time>
|
||||
</i18n-t>
|
||||
<span v-else>
|
||||
{{ $t('nav.built_at', [$d(buildTimeDate, 'shortDate')]) }}
|
||||
</span>
|
||||
·
|
||||
<NuxtLink
|
||||
v-if="buildInfo.env === 'release'"
|
||||
external
|
||||
:href="`https://github.com/elk-zone/elk/releases/tag/v${buildInfo.version}`"
|
||||
target="_blank"
|
||||
font-mono
|
||||
>
|
||||
v{{ buildInfo.version }}
|
||||
</NuxtLink>
|
||||
<span v-else>{{ buildInfo.env }}</span>
|
||||
<template v-if="buildInfo.commit && buildInfo.branch !== 'release'">
|
||||
·
|
||||
<NuxtLink
|
||||
external
|
||||
:href="`https://github.com/elk-zone/elk/commit/${buildInfo.commit}`"
|
||||
target="_blank"
|
||||
font-mono
|
||||
>
|
||||
{{ buildInfo.shortCommit }}
|
||||
</NuxtLink>
|
||||
</template>
|
||||
</div>
|
||||
<div>
|
||||
<NuxtLink cursor-pointer hover:underline to="/settings/about">
|
||||
{{ $t('settings.about.label') }}
|
||||
</NuxtLink>
|
||||
<template v-if="config.public.privacyPolicyUrl">
|
||||
·
|
||||
<NuxtLink cursor-pointer hover:underline :to="config.public.privacyPolicyUrl">
|
||||
{{ $t('nav.privacy') }}
|
||||
</NuxtLink>
|
||||
</template>
|
||||
·
|
||||
<NuxtLink href="/m.webtoo.ls/@elk" target="_blank">
|
||||
Mastodon
|
||||
</NuxtLink>
|
||||
·
|
||||
<NuxtLink href="https://chat.elk.zone" target="_blank" external>
|
||||
Discord
|
||||
</NuxtLink>
|
||||
·
|
||||
<NuxtLink href="https://github.com/elk-zone/elk" target="_blank" external>
|
||||
GitHub
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
57
app/components/nav/NavLogo.vue
Normal file
57
app/components/nav/NavLogo.vue
Normal file
|
@ -0,0 +1,57 @@
|
|||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span shrink-0 aspect="1/1" sm:h-8 xl:h-10 class="rtl-flip"><svg
|
||||
xmlns="http://www.w3.org/2000/svg" w-full
|
||||
aspect="1/1" sm:h-8 xl:h-10 sm:w-8 xl:w-10 viewBox="0 0 250 250" fill="none"
|
||||
>
|
||||
<mask
|
||||
id="a"
|
||||
width="240"
|
||||
height="234"
|
||||
x="4"
|
||||
y="1"
|
||||
maskUnits="userSpaceOnUse"
|
||||
style="mask-type:alpha"
|
||||
>
|
||||
<path
|
||||
id="path19"
|
||||
fill="#D9D9D9"
|
||||
d="M244 123c0 64.617-38.383 112-103 112-64.617 0-103-30.883-103-95.5C38 111.194-8.729 36.236 8 16 29.46-9.959 88.689 6 125 6c64.617 0 119 52.383 119 117Z"
|
||||
/>
|
||||
</mask>
|
||||
<g
|
||||
id="g28"
|
||||
mask="url(#a)"
|
||||
transform="matrix(0.90923731,0,0,1.0049564,13.520015,-3.1040835)"
|
||||
>
|
||||
<path
|
||||
id="path22"
|
||||
class="body"
|
||||
d="m 116.94,88.1 c -13.344,1.552 -20.436,-2.019 -24.706,10.71 0,0 14.336,21.655 52.54,21.112 -2.135,8.848 -1.144,15.368 -1.144,23.207 0,26.079 -20.589,48.821 -65.961,48.821 -23.03,0 -51.015,4.191 -72.367,15.911 -15.175,8.305 -27.048,20.336 -32.302,37.023 l 5.956,8.461 11.4,0.155 v 47.889 l -13.91,21.966 3.998,63.645 H -6.364 L -5.22,335.773 C 1.338,331.892 16.36,321.802 29.171,306.279 46.557,285.4 59.902,255.052 44.193,217.486 l 11.744,-5.045 c 12.887,30.814 8.388,57.514 -2.898,79.013 21.58,-0.698 40.11,-2.095 55.819,-4.734 l -3.584,-43.698 12.659,-1.087 L 129.98,387 h 13.116 l 2.212,-94.459 c 10.447,-4.502 34.239,-21.034 45.372,-78.47 1.372,-6.986 2.135,-12.885 2.516,-17.93 1.754,-12.806 2.745,-27.243 3.051,-43.698 l -18.683,-5.976 h 57.42 l 5.567,-12.807 c -5.414,0.233 -11.896,-2.639 -11.896,-2.639 l 1.297,-6.209 H 242 L 176.801,90.428 c -7.244,2.794 -14.87,6.442 -20.208,10.866 -4.27,-3.105 -19.063,-12.807 -39.653,-13.195 z"
|
||||
/>
|
||||
<path
|
||||
id="path24"
|
||||
class="wood"
|
||||
d="M 6.217,24.493 18.494,21 c 5.948,21.577 13.345,33.375 22.648,39.352 8.388,5.099 19.75,5.239 31.799,4.579 C 69.433,63.767 66.154,62.137 63.104,59.886 56.317,54.841 50.522,46.458 46.175,31.246 l 12.201,-3.649 c 3.279,11.488 7.092,18.085 12.201,21.888 5.11,3.726 11.286,4.657 18.606,5.433 13.726,1.553 30.884,2.174 52.312,12.264 2.898,1.086 5.872,2.483 8.769,4.036 -0.381,-0.776 -0.762,-1.553 -1.296,-2.406 -3.66,-5.822 -10.828,-11.953 -24.097,-16.92 l 4.27,-12.109 c 21.581,7.917 30.121,19.171 33.553,28.097 3.965,10.168 1.525,18.124 1.525,18.124 -3.05,1.009 -6.1,2.406 -9.608,3.492 -6.634,-4.579 -12.887,-8.033 -18.835,-10.75 C 113.814,70.442 92.31,76.108 73.246,77.893 58.91,79.213 45.794,78.591 34.432,71.295 23.222,64.155 13.385,50.495 6.217,24.493 Z"
|
||||
/>
|
||||
<path
|
||||
id="path26"
|
||||
class="wood"
|
||||
d="M 90.098,45.294 C 87.582,39.55 86.057,32.487 86.743,23.794 l 12.659,0.932 c -0.763,10.555 2.897,17.696 7.015,22.353 -5.338,-0.931 -10.447,-1.04 -16.319,-1.785 z m 80.069,-1.32 8.312,-9.702 c 21.58,19.094 8.159,46.415 8.159,46.415 l -11.819,-1.32 c -0.382,-6.24 -1.144,-17.836 -6.635,-24.371 3.584,1.84 6.635,3.865 9.99,6.908 0,-5.666 -1.754,-12.341 -8.007,-17.93 z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
svg path.wood {
|
||||
fill: var(--c-primary);
|
||||
}
|
||||
svg path.body {
|
||||
fill: var(--c-text-secondary);
|
||||
}
|
||||
</style>
|
82
app/components/nav/NavSide.vue
Normal file
82
app/components/nav/NavSide.vue
Normal file
|
@ -0,0 +1,82 @@
|
|||
<script setup lang="ts">
|
||||
import { STORAGE_KEY_LAST_ACCESSED_EXPLORE_ROUTE, STORAGE_KEY_LAST_ACCESSED_NOTIFICATION_ROUTE } from '~/constants'
|
||||
|
||||
defineProps<{
|
||||
command?: boolean
|
||||
}>()
|
||||
const { notifications } = useNotifications()
|
||||
const useStarFavoriteIcon = usePreferences('useStarFavoriteIcon')
|
||||
const lastAccessedNotificationRoute = useLocalStorage(STORAGE_KEY_LAST_ACCESSED_NOTIFICATION_ROUTE, '')
|
||||
const lastAccessedExploreRoute = useLocalStorage(STORAGE_KEY_LAST_ACCESSED_EXPLORE_ROUTE, '')
|
||||
|
||||
const notificationsLink = computed(() => {
|
||||
const hydrated = isHydrated.value
|
||||
const user = currentUser.value
|
||||
const lastRoute = lastAccessedNotificationRoute.value
|
||||
if (!hydrated || !user || !lastRoute) {
|
||||
return '/notifications'
|
||||
}
|
||||
|
||||
return `/notifications/${lastRoute}`
|
||||
})
|
||||
const exploreLink = computed(() => {
|
||||
const hydrated = isHydrated.value
|
||||
const server = currentServer.value
|
||||
let lastRoute = lastAccessedExploreRoute.value
|
||||
if (!hydrated) {
|
||||
return '/explore'
|
||||
}
|
||||
|
||||
if (lastRoute.length) {
|
||||
lastRoute = `/${lastRoute}`
|
||||
}
|
||||
|
||||
return server ? `/${server}/explore${lastRoute}` : `/explore${lastRoute}`
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav sm:px3 flex="~ col gap2" shrink text-size-base leading-normal md:text-lg h-full mt-1 overflow-y-auto>
|
||||
<NavSideItem :text="$t('nav.search')" to="/search" icon="i-ri:search-line" xl:hidden :command="command" />
|
||||
|
||||
<div class="spacer" shrink xl:hidden />
|
||||
<NavSideItem :text="$t('nav.home')" to="/home" icon="i-ri:home-5-line" user-only :command="command" />
|
||||
<NavSideItem :text="$t('nav.notifications')" :to="notificationsLink" icon="i-ri:notification-4-line" user-only :command="command">
|
||||
<template #icon>
|
||||
<div flex relative>
|
||||
<div class="i-ri:notification-4-line" 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>
|
||||
</div>
|
||||
</template>
|
||||
</NavSideItem>
|
||||
<NavSideItem :text="$t('nav.conversations')" to="/conversations" icon="i-ri:at-line" user-only :command="command" />
|
||||
<NavSideItem :text="$t('nav.favourites')" to="/favourites" :icon="useStarFavoriteIcon ? 'i-ri:star-line' : 'i-ri:heart-3-line'" user-only :command="command" />
|
||||
<NavSideItem :text="$t('nav.bookmarks')" to="/bookmarks" icon="i-ri:bookmark-line" user-only :command="command" />
|
||||
|
||||
<div class="spacer" shrink hidden sm:block />
|
||||
<NavSideItem :text="$t('action.compose')" to="/compose" icon="i-ri:quill-pen-line" user-only :command="command" />
|
||||
|
||||
<div class="spacer" shrink hidden sm:block />
|
||||
<NavSideItem :text="$t('nav.explore')" :to="exploreLink" icon="i-ri:compass-3-line" :command="command" />
|
||||
<NavSideItem :text="$t('nav.local')" :to="isHydrated ? `/${currentServer}/public/local` : '/public/local'" icon="i-ri:group-2-line " :command="command" />
|
||||
<NavSideItem :text="$t('nav.federated')" :to="isHydrated ? `/${currentServer}/public` : '/public'" icon="i-ri:earth-line" :command="command" />
|
||||
<NavSideItem :text="$t('nav.lists')" :to="isHydrated ? `/${currentServer}/lists` : '/lists'" icon="i-ri:list-check" user-only :command="command" />
|
||||
<NavSideItem :text="$t('nav.hashtags')" to="/hashtags" icon="i-ri:hashtag" user-only :command="command" />
|
||||
|
||||
<div class="spacer" shrink hidden sm:block />
|
||||
<NavSideItem :text="$t('nav.settings')" to="/settings" icon="i-ri:settings-3-line" :command="command" />
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.spacer {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
@media screen and ( max-height: 920px ) and ( min-width: 640px ) {
|
||||
.spacer {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
108
app/components/nav/NavSideItem.vue
Normal file
108
app/components/nav/NavSideItem.vue
Normal file
|
@ -0,0 +1,108 @@
|
|||
<script setup lang="ts">
|
||||
const { text, icon, to, userOnly = false, command } = defineProps<{
|
||||
text?: string
|
||||
icon: string
|
||||
to: string | Record<string, string>
|
||||
userOnly?: boolean
|
||||
command?: boolean
|
||||
}>()
|
||||
|
||||
defineSlots<{
|
||||
icon: (props: object) => void
|
||||
default: (props: object) => void
|
||||
}>()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
useCommand({
|
||||
scope: 'Navigation',
|
||||
|
||||
name: () => text ?? (typeof to === 'string' ? to as string : to.name),
|
||||
icon: () => icon,
|
||||
visible: () => command,
|
||||
|
||||
onActivate() {
|
||||
router.push(to)
|
||||
},
|
||||
})
|
||||
|
||||
const activeClass = ref('text-primary')
|
||||
onHydrated(async () => {
|
||||
// TODO: force NuxtLink to reevaluate, we now we are in this route though, so we should force it to active
|
||||
// we don't have currentServer defined until later
|
||||
activeClass.value = ''
|
||||
await nextTick()
|
||||
activeClass.value = 'text-primary'
|
||||
})
|
||||
|
||||
// Optimize rendering for the common case of being logged in, only show visual feedback for disabled user-only items
|
||||
// when we know there is no user.
|
||||
const noUserDisable = computed(() => !isHydrated.value || (userOnly && !currentUser.value))
|
||||
const noUserVisual = computed(() => isHydrated.value && userOnly && !currentUser.value)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink
|
||||
:to="to"
|
||||
:disabled="noUserDisable"
|
||||
:class="noUserVisual ? 'op25 pointer-events-none ' : ''"
|
||||
:active-class="activeClass"
|
||||
group focus:outline-none disabled:pointer-events-none
|
||||
:tabindex="noUserDisable ? -1 : null"
|
||||
@click="$scrollToTop"
|
||||
>
|
||||
<CommonTooltip :disabled="!isMediumOrLargeScreen" :content="text" placement="right">
|
||||
<div
|
||||
class="item"
|
||||
flex items-center gap4
|
||||
xl="ml0 mr5 px5 w-auto"
|
||||
:class="isSmallScreen
|
||||
? `
|
||||
w-full
|
||||
px5 sm:mxa
|
||||
transition-colors duration-200 transform
|
||||
hover-bg-gray-100 hover-dark:(bg-gray-700 text-white)
|
||||
` : `
|
||||
w-fit rounded-3
|
||||
px2 mx3 sm:mxa
|
||||
transition-100
|
||||
elk-group-hover-bg-active
|
||||
group-focus-visible:ring-2
|
||||
group-focus-visible:ring-current
|
||||
`"
|
||||
>
|
||||
<slot name="icon">
|
||||
<div :class="icon" text-xl />
|
||||
</slot>
|
||||
<slot>
|
||||
<span block sm:hidden xl:block select-none>{{ isHydrated ? text : ' ' }}</span>
|
||||
</slot>
|
||||
</div>
|
||||
</CommonTooltip>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.item {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
@media screen and ( max-height: 820px ) and ( min-width: 1280px ) {
|
||||
.item {
|
||||
padding-top: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
}
|
||||
@media screen and ( max-height: 780px ) and ( min-width: 640px ) {
|
||||
.item {
|
||||
padding-top: 0.35rem;
|
||||
padding-bottom: 0.35rem;
|
||||
}
|
||||
}
|
||||
@media screen and ( max-height: 780px ) and ( min-width: 1280px ) {
|
||||
.item {
|
||||
padding-top: 0.05rem;
|
||||
padding-bottom: 0.05rem;
|
||||
}
|
||||
}
|
||||
</style>
|
50
app/components/nav/NavTitle.vue
Normal file
50
app/components/nav/NavTitle.vue
Normal file
|
@ -0,0 +1,50 @@
|
|||
<script setup lang="ts">
|
||||
const { env } = useBuildInfo()
|
||||
const router = useRouter()
|
||||
const back = ref<any>('')
|
||||
|
||||
const nuxtApp = useNuxtApp()
|
||||
|
||||
function onClickLogo() {
|
||||
nuxtApp.hooks.callHook('elk-logo:click')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
back.value = router.options.history.state.back
|
||||
})
|
||||
router.afterEach(() => {
|
||||
back.value = router.options.history.state.back
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex justify-between sticky top-0 bg-base z-1 py-4 native:py-7 data-tauri-drag-region>
|
||||
<NuxtLink
|
||||
flex items-end gap-3
|
||||
py2 px-5
|
||||
text-2xl
|
||||
select-none
|
||||
focus-visible:ring="2 current"
|
||||
to="/home"
|
||||
@click.prevent="onClickLogo"
|
||||
>
|
||||
<NavLogo shrink-0 aspect="1/1" sm:h-8 xl:h-10 class="rtl-flip" />
|
||||
<div v-show="isHydrated" hidden xl:block text-secondary>
|
||||
{{ $t('app_name') }} <sup text-sm italic mt-1>{{ env === 'release' ? 'alpha' : env }}</sup>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
<div
|
||||
hidden xl:flex items-center me-8 mt-2 gap-1
|
||||
>
|
||||
<CommonTooltip :content="$t('nav.back')">
|
||||
<NuxtLink
|
||||
:aria-label="$t('nav.back')"
|
||||
:class="{ 'pointer-events-none op0': !back || back === '/', 'xl:flex': $route.name !== 'tag' }"
|
||||
@click="$router.go(-1)"
|
||||
>
|
||||
<div text-xl i-ri:arrow-left-line class="rtl-flip" btn-text />
|
||||
</NuxtLink>
|
||||
</CommonTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
47
app/components/nav/NavUser.vue
Normal file
47
app/components/nav/NavUser.vue
Normal file
|
@ -0,0 +1,47 @@
|
|||
<script setup lang="ts">
|
||||
const { busy, oauth, singleInstanceServer } = useSignIn()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDropdown v-if="isHydrated && currentUser" sm:hidden>
|
||||
<div style="-webkit-touch-callout: none;">
|
||||
<AccountAvatar
|
||||
:account="currentUser.account"
|
||||
h-8
|
||||
w-8
|
||||
:draggable="false"
|
||||
square
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template #popper="{ hide }">
|
||||
<UserSwitcher @click="hide()" />
|
||||
</template>
|
||||
</VDropdown>
|
||||
<template v-else>
|
||||
<button
|
||||
v-if="singleInstanceServer"
|
||||
flex="~ row"
|
||||
gap-x-1 items-center justify-center btn-solid text-sm px-2 py-1 xl:hidden
|
||||
:disabled="busy"
|
||||
@click="oauth()"
|
||||
>
|
||||
<span v-if="busy" aria-hidden="true" block animate animate-spin preserve-3d class="rtl-flip">
|
||||
<span block i-ri:loader-2-fill aria-hidden="true" />
|
||||
</span>
|
||||
<span v-else aria-hidden="true" block i-ri:login-circle-line class="rtl-flip" />
|
||||
<i18n-t keypath="action.sign_in_to">
|
||||
<strong>{{ currentServer }}</strong>
|
||||
</i18n-t>
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
flex="~ row"
|
||||
gap-x-1 items-center justify-center btn-solid text-sm px-2 py-1 xl:hidden
|
||||
@click="openSigninDialog()"
|
||||
>
|
||||
<span aria-hidden="true" block i-ri:login-circle-line class="rtl-flip" />
|
||||
{{ $t('action.sign_in') }}
|
||||
</button>
|
||||
</template>
|
||||
</template>
|
3
app/components/nav/NavUserSkeleton.vue
Normal file
3
app/components/nav/NavUserSkeleton.vue
Normal file
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<div bg-base h-8 w-8 rounded-full />
|
||||
</template>
|
11
app/components/nav/button/Bookmark.vue
Normal file
11
app/components/nav/button/Bookmark.vue
Normal file
|
@ -0,0 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
activeClass: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink to="/bookmarks" :aria-label="$t('nav.bookmarks')" :active-class="activeClass" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
|
||||
<div i-ri:bookmark-line />
|
||||
</NuxtLink>
|
||||
</template>
|
11
app/components/nav/button/Compose.vue
Normal file
11
app/components/nav/button/Compose.vue
Normal file
|
@ -0,0 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
activeClass: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink to="/compose" :aria-label="$t('nav.favourites')" :active-class="activeClass" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
|
||||
<div i-ri:quill-pen-line />
|
||||
</NuxtLink>
|
||||
</template>
|
15
app/components/nav/button/Explore.vue
Normal file
15
app/components/nav/button/Explore.vue
Normal file
|
@ -0,0 +1,15 @@
|
|||
<script setup lang="ts">
|
||||
import { STORAGE_KEY_LAST_ACCESSED_EXPLORE_ROUTE } from '~/constants'
|
||||
|
||||
defineProps<{
|
||||
activeClass: string
|
||||
}>()
|
||||
|
||||
const lastAccessedExploreRoute = useLocalStorage(STORAGE_KEY_LAST_ACCESSED_EXPLORE_ROUTE, '')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink :to="`/${currentServer}/explore/${lastAccessedExploreRoute}`" :aria-label="$t('nav.explore')" :active-class="activeClass" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
|
||||
<div i-ri:compass-3-line />
|
||||
</NuxtLink>
|
||||
</template>
|
11
app/components/nav/button/Favorite.vue
Normal file
11
app/components/nav/button/Favorite.vue
Normal file
|
@ -0,0 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
activeClass: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink to="/favourites" :aria-label="$t('nav.favourites')" :active-class="activeClass" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
|
||||
<div i-ri:heart-line />
|
||||
</NuxtLink>
|
||||
</template>
|
11
app/components/nav/button/Federated.vue
Normal file
11
app/components/nav/button/Federated.vue
Normal file
|
@ -0,0 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
activeClass: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink :to="`/${currentServer}/public`" :aria-label="$t('nav.federated')" :active-class="activeClass" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
|
||||
<div i-ri:earth-line />
|
||||
</NuxtLink>
|
||||
</template>
|
11
app/components/nav/button/Hashtag.vue
Normal file
11
app/components/nav/button/Hashtag.vue
Normal file
|
@ -0,0 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
activeClass: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink to="/hashtags" :aria-label="$t('nav.hashtags')" :active-class="activeClass" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
|
||||
<div i-ri:hashtag />
|
||||
</NuxtLink>
|
||||
</template>
|
11
app/components/nav/button/Home.vue
Normal file
11
app/components/nav/button/Home.vue
Normal file
|
@ -0,0 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
activeClass: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink to="/home" :aria-label="$t('nav.home')" :active-class="activeClass" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
|
||||
<div i-ri:home-5-line />
|
||||
</NuxtLink>
|
||||
</template>
|
17
app/components/nav/button/List.vue
Normal file
17
app/components/nav/button/List.vue
Normal file
|
@ -0,0 +1,17 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
activeClass: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink
|
||||
to="/lists"
|
||||
:aria-label="$t('nav.lists')"
|
||||
:active-class="activeClass"
|
||||
flex flex-row items-center place-content-center h-full flex-1
|
||||
class="coarse-pointer:select-none" @click="$scrollToTop"
|
||||
>
|
||||
<div i-ri:list-check />
|
||||
</NuxtLink>
|
||||
</template>
|
11
app/components/nav/button/Local.vue
Normal file
11
app/components/nav/button/Local.vue
Normal file
|
@ -0,0 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
activeClass: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink group :to="`/${currentServer}/public/local`" :aria-label="$t('nav.local')" :active-class="activeClass" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
|
||||
<div i-ri:group-2-line />
|
||||
</NuxtLink>
|
||||
</template>
|
15
app/components/nav/button/Mention.vue
Normal file
15
app/components/nav/button/Mention.vue
Normal file
|
@ -0,0 +1,15 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
activeClass: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink
|
||||
to="/conversations" :aria-label="$t('nav.conversations')"
|
||||
:active-class="activeClass" flex flex-row items-center place-content-center h-full
|
||||
flex-1 class="coarse-pointer:select-none" @click="$scrollToTop"
|
||||
>
|
||||
<div i-ri:at-line />
|
||||
</NuxtLink>
|
||||
</template>
|
19
app/components/nav/button/MoreMenu.vue
Normal file
19
app/components/nav/button/MoreMenu.vue
Normal file
|
@ -0,0 +1,19 @@
|
|||
<script setup lang="ts">
|
||||
const model = defineModel<boolean>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NavBottomMoreMenu
|
||||
v-slot="{ toggleVisible, show }" v-model="model!" flex flex-row items-center
|
||||
place-content-center h-full flex-1 cursor-pointer
|
||||
>
|
||||
<button
|
||||
flex items-center place-content-center h-full flex-1 class="select-none"
|
||||
:class="show ? '!text-primary' : ''"
|
||||
:aria-label="$t('nav.more_menu')"
|
||||
@click="toggleVisible"
|
||||
>
|
||||
<span :class="show ? 'i-ri:close-fill' : 'i-ri:more-fill'" />
|
||||
</button>
|
||||
</NavBottomMoreMenu>
|
||||
</template>
|
21
app/components/nav/button/Notification.vue
Normal file
21
app/components/nav/button/Notification.vue
Normal file
|
@ -0,0 +1,21 @@
|
|||
<script setup lang="ts">
|
||||
import { STORAGE_KEY_LAST_ACCESSED_NOTIFICATION_ROUTE } from '~/constants'
|
||||
|
||||
defineProps<{
|
||||
activeClass: string
|
||||
}>()
|
||||
|
||||
const { notifications } = useNotifications()
|
||||
const lastAccessedNotificationRoute = useLocalStorage(STORAGE_KEY_LAST_ACCESSED_NOTIFICATION_ROUTE, '')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink :to="`/notifications/${lastAccessedNotificationRoute}`" :aria-label="$t('nav.notifications')" :active-class="activeClass" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
|
||||
<div flex relative>
|
||||
<div class="i-ri:notification-4-line" 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>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</template>
|
11
app/components/nav/button/Search.vue
Normal file
11
app/components/nav/button/Search.vue
Normal file
|
@ -0,0 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
activeClass: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink to="/search" :aria-label="$t('nav.search')" :active-class="activeClass" flex flex-row items-center place-content-center h-full flex-1 class="coarse-pointer:select-none" @click="$scrollToTop">
|
||||
<div i-ri:search-line />
|
||||
</NuxtLink>
|
||||
</template>
|
Loading…
Add table
Add a link
Reference in a new issue