feat: Implement mobile navbar

This commit is contained in:
Jesse Wierzbinski 2024-12-09 16:52:04 +01:00
parent 4ba3ed3d37
commit 0987df7783
No known key found for this signature in database
17 changed files with 310 additions and 25 deletions

BIN
bun.lockb

Binary file not shown.

View 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>

View 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>

View 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>

View file

@ -1,23 +1,46 @@
<template> <template>
<DropdownMenu> <Drawer v-if="isMobile">
<DropdownMenuTrigger as-child> <DrawerTrigger :as-child="true">
<SidebarMenuButton size="lg" <slot />
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"> </DrawerTrigger>
<Avatar v-if="identity" class="size-8" :src="identity.account.avatar" :name="identity.account.display_name" /> <DrawerContent>
<Avatar v-else class="size-8" name="AB" /> <Button @click="switchAccount(identity.account.id)" variant="outline" size="lg"
<div class="grid flex-1 text-left text-sm leading-tight"> :href="`/@${identity.account.username}`" v-for="identity of identities"
<span class="truncate font-semibold" v-render-emojis="identity?.account.emojis">{{ class="flex w-full items-center gap-2 px-4 text-left h-20">
identity?.account.display_name ?? "Not signed in" <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>
<span class="truncate text-xs" v-if="identity">@{{ identity?.account.acct }}</span>
</div> </div>
<ChevronsUpDown class="ml-auto size-4" /> </Button>
</SidebarMenuButton> <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> </DropdownMenuTrigger>
<DropdownMenuContent class="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg" side="bottom" <DropdownMenuContent class="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg" side="bottom"
align="end" :side-offset="4"> align="end" :side-offset="4">
<DropdownMenuLabel class="p-0 font-normal"> <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" /> <Avatar class="size-8" :src="identity.account.avatar" :name="identity.account.display_name" />
<div class="grid flex-1 text-left text-sm leading-tight"> <div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold" v-render-emojis="identity.account.emojis">{{ <span class="truncate font-semibold" v-render-emojis="identity.account.emojis">{{
@ -54,18 +77,14 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { import { BadgeCheck, LogIn, LogOut, UserPlus } from "lucide-vue-next";
BadgeCheck,
ChevronsUpDown,
LogIn,
LogOut,
UserPlus,
} from "lucide-vue-next";
import { toast } from "vue-sonner"; import { toast } from "vue-sonner";
import * as m from "~/paraglide/messages.js"; import * as m from "~/paraglide/messages.js";
import { NuxtLink } from "#components"; import { NuxtLink } from "#components";
import DrawerContent from "../modals/drawer-content.vue";
import Avatar from "../profiles/avatar.vue"; import Avatar from "../profiles/avatar.vue";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { Drawer, DrawerTrigger } from "../ui/drawer";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@ -75,9 +94,9 @@ import {
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "../ui/dropdown-menu"; } from "../ui/dropdown-menu";
import { SidebarMenuButton } from "../ui/sidebar";
const appData = useAppData(); const appData = useAppData();
const isMobile = useMediaQuery("(max-width: 768px)");
const signInAction = () => signIn(appData); const signInAction = () => signIn(appData);

View file

@ -10,7 +10,8 @@
'https://cdn.versia.pub/branding/icon.svg' 'https://cdn.versia.pub/branding/icon.svg'
" :name="instance?.title" /> " :name="instance?.title" />
<div class="grid flex-1 text-left text-sm leading-tight"> <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> <span class="truncate text-xs">{{ m.top_active_ocelot_cure() }}</span>
</div> </div>
<!-- <ChevronsUpDown class="ml-auto" /> --> <!-- <ChevronsUpDown class="ml-auto" /> -->
@ -67,7 +68,21 @@
<SidebarFooter> <SidebarFooter>
<SidebarMenu class="gap-3"> <SidebarMenu class="gap-3">
<SidebarMenuItem> <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>
<SidebarMenuItem class="flex flex-col gap-2"> <SidebarMenuItem class="flex flex-col gap-2">
<Button variant="default" size="lg" class="w-full group-data-[collapsible=icon]:px-4" <Button variant="default" size="lg" class="w-full group-data-[collapsible=icon]:px-4"
@ -92,6 +107,7 @@ import {
BedSingle, BedSingle,
Bell, Bell,
ChevronRight, ChevronRight,
ChevronsUpDown,
DownloadCloud, DownloadCloud,
Globe, Globe,
House, House,

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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";

View file

@ -16,12 +16,14 @@
</CardFooter> </CardFooter>
</Card> </Card>
</Sidebar> </Sidebar>
<MobileNavbar />
<ComposerDialog /> <ComposerDialog />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import ComposerDialog from "~/components/composer/dialog.vue"; import ComposerDialog from "~/components/composer/dialog.vue";
import SquarePattern from "~/components/graphics/square-pattern.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 Sidebar from "~/components/sidebars/sidebar.vue";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { import {

View file

@ -238,6 +238,9 @@ export default defineNuxtConfig({
], ],
htmlAttrs: { lang: "en-us" }, htmlAttrs: { lang: "en-us" },
}, },
rootAttrs: {
"vaul-drawer-wrapper": true,
},
keepalive: true, keepalive: true,
}, },
nitro: { nitro: {

View file

@ -53,6 +53,7 @@
"shadcn-nuxt": "0.11.3", "shadcn-nuxt": "0.11.3",
"tailwind-merge": "^2.5.5", "tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"vaul-vue": "^0.2.0",
"vee-validate": "^4.14.7", "vee-validate": "^4.14.7",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",