mirror of
https://github.com/versia-pub/frontend.git
synced 2025-12-06 08:28:20 +01:00
feat: ✨ Implement mobile navbar
This commit is contained in:
parent
4ba3ed3d37
commit
0987df7783
9
components/modals/drawer-content.vue
Normal file
9
components/modals/drawer-content.vue
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<DrawerContent class="flex flex-col gap-2 px-4 mb-4 [&>:nth-child(2)]:mt-4">
|
||||
<slot />
|
||||
</DrawerContent>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { DrawerContent } from "../ui/drawer";
|
||||
</script>
|
||||
31
components/navigation/mobile-navbar.vue
Normal file
31
components/navigation/mobile-navbar.vue
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<template>
|
||||
<div
|
||||
class="fixed md:hidden bottom-0 inset-x-0 border-t h-20 bg-background z-10 flex items-center justify-around *:p-7 *:w-full gap-6 p-6">
|
||||
<Timelines>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Home class="!size-6" />
|
||||
</Button>
|
||||
</Timelines>
|
||||
<Button v-if="identity" :as="NuxtLink" href="/notifications" variant="ghost" size="icon">
|
||||
<Bell class="!size-6" />
|
||||
</Button>
|
||||
<AccountSwitcher>
|
||||
<Button variant="ghost" size="icon">
|
||||
<User class="!size-6" />
|
||||
</Button>
|
||||
</AccountSwitcher>
|
||||
<Button v-if="identity" variant="default" size="icon" :title="m.salty_aloof_turkey_nudge()"
|
||||
@click="useEvent('composer:open')">
|
||||
<Pen class="!size-6" />
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Bell, Home, Pen, User } from "lucide-vue-next";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
import { NuxtLink } from "#components";
|
||||
import AccountSwitcher from "../sidebars/account-switcher.vue";
|
||||
import { Button } from "../ui/button";
|
||||
import Timelines from "./timelines.vue";
|
||||
</script>
|
||||
55
components/navigation/timelines.vue
Normal file
55
components/navigation/timelines.vue
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<template>
|
||||
<Drawer>
|
||||
<DrawerTrigger :as-child="true">
|
||||
<slot />
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<DrawerClose v-for="item in timelines.filter(
|
||||
i => i.requiresLogin ? !!identity : true,
|
||||
)" :key="item.name" :as-child="true">
|
||||
<Button :as="NuxtLink" :href="item.url" variant="outline" size="lg" class="w-full">
|
||||
<component :is="item.icon" />
|
||||
{{ item.name }}
|
||||
</Button>
|
||||
</DrawerClose>
|
||||
<DialogTitle class="sr-only">{{ m.trite_real_sawfish_drum() }}</DialogTitle>
|
||||
<DialogDescription class="sr-only">{{ m.trite_real_sawfish_drum() }}</DialogDescription>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { BedSingle, Globe, House, MapIcon } from "lucide-vue-next";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
import { NuxtLink } from "#components";
|
||||
import DrawerContent from "../modals/drawer-content.vue";
|
||||
import { Button } from "../ui/button";
|
||||
import { Drawer, DrawerTrigger } from "../ui/drawer";
|
||||
|
||||
const timelines = [
|
||||
{
|
||||
name: m.bland_chunky_sparrow_propel(),
|
||||
url: "/home",
|
||||
icon: House,
|
||||
requiresLogin: true,
|
||||
},
|
||||
{
|
||||
name: m.lost_trick_dog_grace(),
|
||||
url: "/public",
|
||||
icon: MapIcon,
|
||||
requiresLogin: false,
|
||||
},
|
||||
{
|
||||
name: m.crazy_game_parrot_pave(),
|
||||
url: "/local",
|
||||
icon: BedSingle,
|
||||
requiresLogin: false,
|
||||
},
|
||||
{
|
||||
name: m.real_tame_moose_greet(),
|
||||
url: "/global",
|
||||
icon: Globe,
|
||||
requiresLogin: false,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
|
@ -1,23 +1,46 @@
|
|||
<template>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<SidebarMenuButton size="lg"
|
||||
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
|
||||
<Avatar v-if="identity" class="size-8" :src="identity.account.avatar" :name="identity.account.display_name" />
|
||||
<Avatar v-else class="size-8" name="AB" />
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<span class="truncate font-semibold" v-render-emojis="identity?.account.emojis">{{
|
||||
identity?.account.display_name ?? "Not signed in"
|
||||
<Drawer v-if="isMobile">
|
||||
<DrawerTrigger :as-child="true">
|
||||
<slot />
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<Button @click="switchAccount(identity.account.id)" variant="outline" size="lg"
|
||||
:href="`/@${identity.account.username}`" v-for="identity of identities"
|
||||
class="flex w-full items-center gap-2 px-4 text-left h-20">
|
||||
<Avatar class="size-12" :src="identity.account.avatar" :name="identity.account.display_name" />
|
||||
<div class="grid flex-1 text-left leading-tight">
|
||||
<span class="truncate font-semibold" v-render-emojis="identity.account.emojis">{{
|
||||
identity.account.display_name
|
||||
}}</span>
|
||||
<span class="truncate text-sm">@{{
|
||||
identity.account.acct
|
||||
}}</span>
|
||||
<span class="truncate text-xs" v-if="identity">@{{ identity?.account.acct }}</span>
|
||||
</div>
|
||||
<ChevronsUpDown class="ml-auto size-4" />
|
||||
</SidebarMenuButton>
|
||||
</Button>
|
||||
<Button variant="secondary" size="lg" class="w-full" @click="signInAction">
|
||||
<UserPlus />
|
||||
{{ m.sunny_pink_hyena_walk() }}
|
||||
</Button>
|
||||
<Button variant="secondary" size="lg" @click="signOut()" v-if="identity">
|
||||
<LogOut />
|
||||
{{ m.sharp_big_mallard_reap() }}
|
||||
</Button>
|
||||
<Button variant="secondary" size="lg" :as="NuxtLink" href="/register" v-else>
|
||||
<LogIn />
|
||||
{{ m.honest_few_baboon_pop() }}
|
||||
</Button>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
<DropdownMenu v-else>
|
||||
<DropdownMenuTrigger :as-child="true">
|
||||
<slot />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent class="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg" side="bottom"
|
||||
align="end" :side-offset="4">
|
||||
<DropdownMenuLabel class="p-0 font-normal">
|
||||
<Button @click="switchAccount(identity.account.id)" variant="ghost" size="lg" :href="`/@${identity.account.username}`" v-for="identity of identities" class="flex w-full items-center gap-2 px-1 text-left text-sm">
|
||||
<Button @click="switchAccount(identity.account.id)" variant="ghost" size="lg"
|
||||
:href="`/@${identity.account.username}`" v-for="identity of identities"
|
||||
class="flex w-full items-center gap-2 px-1 text-left text-sm">
|
||||
<Avatar class="size-8" :src="identity.account.avatar" :name="identity.account.display_name" />
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<span class="truncate font-semibold" v-render-emojis="identity.account.emojis">{{
|
||||
|
|
@ -54,18 +77,14 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
BadgeCheck,
|
||||
ChevronsUpDown,
|
||||
LogIn,
|
||||
LogOut,
|
||||
UserPlus,
|
||||
} from "lucide-vue-next";
|
||||
import { BadgeCheck, LogIn, LogOut, UserPlus } from "lucide-vue-next";
|
||||
import { toast } from "vue-sonner";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
import { NuxtLink } from "#components";
|
||||
import DrawerContent from "../modals/drawer-content.vue";
|
||||
import Avatar from "../profiles/avatar.vue";
|
||||
import { Button } from "../ui/button";
|
||||
import { Drawer, DrawerTrigger } from "../ui/drawer";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
|
@ -75,9 +94,9 @@ import {
|
|||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "../ui/dropdown-menu";
|
||||
import { SidebarMenuButton } from "../ui/sidebar";
|
||||
|
||||
const appData = useAppData();
|
||||
const isMobile = useMediaQuery("(max-width: 768px)");
|
||||
|
||||
const signInAction = () => signIn(appData);
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@
|
|||
'https://cdn.versia.pub/branding/icon.svg'
|
||||
" :name="instance?.title" />
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<span class="truncate font-semibold">{{ instance?.title ?? m.short_zippy_felix_kick() }}</span>
|
||||
<span class="truncate font-semibold">{{ instance?.title ?? m.short_zippy_felix_kick()
|
||||
}}</span>
|
||||
<span class="truncate text-xs">{{ m.top_active_ocelot_cure() }}</span>
|
||||
</div>
|
||||
<!-- <ChevronsUpDown class="ml-auto" /> -->
|
||||
|
|
@ -67,7 +68,21 @@
|
|||
<SidebarFooter>
|
||||
<SidebarMenu class="gap-3">
|
||||
<SidebarMenuItem>
|
||||
<AccountSwitcher />
|
||||
<AccountSwitcher>
|
||||
<SidebarMenuButton size="lg"
|
||||
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
|
||||
<Avatar v-if="identity" class="size-8" :src="identity.account.avatar"
|
||||
:name="identity.account.display_name" />
|
||||
<Avatar v-else class="size-8" name="AB" />
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<span class="truncate font-semibold" v-render-emojis="identity?.account.emojis">{{
|
||||
identity?.account.display_name ?? "Not signed in"
|
||||
}}</span>
|
||||
<span class="truncate text-xs" v-if="identity">@{{ identity?.account.acct }}</span>
|
||||
</div>
|
||||
<ChevronsUpDown class="ml-auto size-4" />
|
||||
</SidebarMenuButton>
|
||||
</AccountSwitcher>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem class="flex flex-col gap-2">
|
||||
<Button variant="default" size="lg" class="w-full group-data-[collapsible=icon]:px-4"
|
||||
|
|
@ -92,6 +107,7 @@ import {
|
|||
BedSingle,
|
||||
Bell,
|
||||
ChevronRight,
|
||||
ChevronsUpDown,
|
||||
DownloadCloud,
|
||||
Globe,
|
||||
House,
|
||||
|
|
|
|||
19
components/ui/drawer/Drawer.vue
Normal file
19
components/ui/drawer/Drawer.vue
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<script lang="ts" setup>
|
||||
import { useForwardPropsEmits } from "radix-vue";
|
||||
import type { DrawerRootEmits, DrawerRootProps } from "vaul-vue";
|
||||
import { DrawerRoot } from "vaul-vue";
|
||||
|
||||
const props = withDefaults(defineProps<DrawerRootProps>(), {
|
||||
shouldScaleBackground: true,
|
||||
});
|
||||
|
||||
const emits = defineEmits<DrawerRootEmits>();
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DrawerRoot v-bind="forwarded">
|
||||
<slot />
|
||||
</DrawerRoot>
|
||||
</template>
|
||||
30
components/ui/drawer/DrawerContent.vue
Normal file
30
components/ui/drawer/DrawerContent.vue
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<script lang="ts" setup>
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { DialogContentEmits, DialogContentProps } from "radix-vue";
|
||||
import { useForwardPropsEmits } from "radix-vue";
|
||||
import { DrawerContent, DrawerPortal } from "vaul-vue";
|
||||
import type { HtmlHTMLAttributes } from "vue";
|
||||
import DrawerOverlay from "./DrawerOverlay.vue";
|
||||
|
||||
const props = defineProps<
|
||||
DialogContentProps & { class?: HtmlHTMLAttributes["class"] }
|
||||
>();
|
||||
const emits = defineEmits<DialogContentEmits>();
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DrawerPortal>
|
||||
<DrawerOverlay />
|
||||
<DrawerContent
|
||||
v-bind="forwarded" :class="cn(
|
||||
'fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<div class="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
|
||||
<slot />
|
||||
</DrawerContent>
|
||||
</DrawerPortal>
|
||||
</template>
|
||||
22
components/ui/drawer/DrawerDescription.vue
Normal file
22
components/ui/drawer/DrawerDescription.vue
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<script lang="ts" setup>
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { DrawerDescriptionProps } from "vaul-vue";
|
||||
import { DrawerDescription } from "vaul-vue";
|
||||
import { type HtmlHTMLAttributes, computed } from "vue";
|
||||
|
||||
const props = defineProps<
|
||||
DrawerDescriptionProps & { class?: HtmlHTMLAttributes["class"] }
|
||||
>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DrawerDescription v-bind="delegatedProps" :class="cn('text-sm text-muted-foreground', props.class)">
|
||||
<slot />
|
||||
</DrawerDescription>
|
||||
</template>
|
||||
14
components/ui/drawer/DrawerFooter.vue
Normal file
14
components/ui/drawer/DrawerFooter.vue
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<script lang="ts" setup>
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { HtmlHTMLAttributes } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HtmlHTMLAttributes["class"];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('mt-auto flex flex-col gap-2 p-4', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
14
components/ui/drawer/DrawerHeader.vue
Normal file
14
components/ui/drawer/DrawerHeader.vue
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<script lang="ts" setup>
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { HtmlHTMLAttributes } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HtmlHTMLAttributes["class"];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('grid gap-1.5 p-4 text-center sm:text-left', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
20
components/ui/drawer/DrawerOverlay.vue
Normal file
20
components/ui/drawer/DrawerOverlay.vue
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<script lang="ts" setup>
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { DialogOverlayProps } from "radix-vue";
|
||||
import { DrawerOverlay } from "vaul-vue";
|
||||
import { type HtmlHTMLAttributes, computed } from "vue";
|
||||
|
||||
const props = defineProps<
|
||||
DialogOverlayProps & { class?: HtmlHTMLAttributes["class"] }
|
||||
>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DrawerOverlay v-bind="delegatedProps" :class="cn('fixed inset-0 z-50 bg-black/80', props.class)" />
|
||||
</template>
|
||||
22
components/ui/drawer/DrawerTitle.vue
Normal file
22
components/ui/drawer/DrawerTitle.vue
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<script lang="ts" setup>
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { DrawerTitleProps } from "vaul-vue";
|
||||
import { DrawerTitle } from "vaul-vue";
|
||||
import { type HtmlHTMLAttributes, computed } from "vue";
|
||||
|
||||
const props = defineProps<
|
||||
DrawerTitleProps & { class?: HtmlHTMLAttributes["class"] }
|
||||
>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DrawerTitle v-bind="delegatedProps" :class="cn('text-lg font-semibold leading-none tracking-tight', props.class)">
|
||||
<slot />
|
||||
</DrawerTitle>
|
||||
</template>
|
||||
8
components/ui/drawer/index.ts
Normal file
8
components/ui/drawer/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export { default as Drawer } from "./Drawer.vue";
|
||||
export { default as DrawerContent } from "./DrawerContent.vue";
|
||||
export { default as DrawerDescription } from "./DrawerDescription.vue";
|
||||
export { default as DrawerFooter } from "./DrawerFooter.vue";
|
||||
export { default as DrawerHeader } from "./DrawerHeader.vue";
|
||||
export { default as DrawerOverlay } from "./DrawerOverlay.vue";
|
||||
export { default as DrawerTitle } from "./DrawerTitle.vue";
|
||||
export { DrawerClose, DrawerPortal, DrawerTrigger } from "vaul-vue";
|
||||
|
|
@ -16,12 +16,14 @@
|
|||
</CardFooter>
|
||||
</Card>
|
||||
</Sidebar>
|
||||
<MobileNavbar />
|
||||
<ComposerDialog />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ComposerDialog from "~/components/composer/dialog.vue";
|
||||
import SquarePattern from "~/components/graphics/square-pattern.vue";
|
||||
import MobileNavbar from "~/components/navigation/mobile-navbar.vue";
|
||||
import Sidebar from "~/components/sidebars/sidebar.vue";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -238,6 +238,9 @@ export default defineNuxtConfig({
|
|||
],
|
||||
htmlAttrs: { lang: "en-us" },
|
||||
},
|
||||
rootAttrs: {
|
||||
"vaul-drawer-wrapper": true,
|
||||
},
|
||||
keepalive: true,
|
||||
},
|
||||
nitro: {
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@
|
|||
"shadcn-nuxt": "0.11.3",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vaul-vue": "^0.2.0",
|
||||
"vee-validate": "^4.14.7",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0",
|
||||
|
|
|
|||
Loading…
Reference in a new issue