refactor: ♻️ Fix more broken UIs, rewrite account switcher

This commit is contained in:
Jesse Wierzbinski 2025-04-10 18:44:53 +02:00
parent ac0a571ecc
commit a4d42e3086
No known key found for this signature in database
31 changed files with 167 additions and 176 deletions

View file

@ -1,6 +1,6 @@
<template>
<EditorContent :editor="editor"
:class="[$style.content, 'prose prose-sm dark:prose-invert break-words prose-a:no-underline prose-a:hover:underline prose-p:*:first-of-type:mt-0']" />
:class="[$style.content, 'prose prose-sm dark:prose-invert break-words prose-a:no-underline prose-a:hover:underline prose-p:first-of-type:mt-0']" />
</template>
<script lang="ts" setup>

View file

@ -1,14 +1,13 @@
<template>
<Alert class="grid grid-cols-[1fr_auto]">
<LogIn class="size-4" />
<Alert layout="button" class="grid">
<LogIn />
<AlertTitle>{{ m.sunny_quick_lionfish_flip() }}</AlertTitle>
<AlertDescription class="col-start-1">
<AlertDescription>
{{ m.brave_known_pelican_drip() }}
</AlertDescription>
<!-- Add pl-4 because Alert is adding additional padding, which we don't want -->
<Button
variant="secondary"
class="w-full col-start-2 row-start-1 row-span-2 !pl-4"
class="w-full"
@click="signInAction"
>
{{ m.fuzzy_sea_moth_absorb() }}

View file

@ -1,6 +1,6 @@
<template>
<Card class="size-16">
<Loader class="size-full animate-spin" />
<Card class="flex items-center justify-center">
<Loader class="size-6 animate-spin" />
</Card>
</template>

View file

@ -30,7 +30,7 @@
</Popover>
</Card>
<DialogContent :hide-close="true"
class="fixed inset-0 z-50 w-screen h-screen p-6 duration-200 bg-transparent border-none grid grid-rows-[auto,1fr,auto] overflow-hidden translate-x-0 translate-y-0 max-w-full !animate-none gap-6">
class="fixed inset-0 z-50 w-screen h-screen p-6 duration-200 bg-transparent border-none grid grid-rows-[auto_1fr_auto] overflow-hidden translate-x-0 translate-y-0 max-w-full !animate-none gap-6">
<div class="flex flex-row gap-2 w-full">
<DialogTitle class="sr-only">{{ attachment.type }}</DialogTitle>
<Button as="a" :href="attachment?.url" target="_blank" :download="true" variant="ghost" size="icon"

View file

@ -1,11 +1,11 @@
<template>
<Card class="*:w-full p-2">
<Collapsible :default-open="true" v-slot="{ open }">
<Collapsible :default-open="true" v-slot="{ open }" class="space-y-1">
<Tooltip>
<TooltipTrigger :as-child="true">
<CardHeader
v-if="notification.account"
class="flex-row items-center gap-2 px-2"
class="flex flex-row items-center gap-2 px-2"
>
<component :is="icon" class="size-5 shrink-0" />
<Avatar

View file

@ -4,7 +4,7 @@
class="grid justify-normal items-center px-6 py-4 gap-4"
v-slot="{ open }"
>
<div class="grid grid-cols-[1fr,auto] items-center gap-4">
<div class="grid grid-cols-[1fr_auto] items-center gap-4">
<CardHeader class="space-y-0.5 p-0">
<CardTitle class="text-base">
{{ setting.title() }}

View file

@ -1,6 +1,6 @@
<template>
<Collapsible :default-open="true">
<div class="grid grid-cols-[1fr,auto] gap-4 items-baseline">
<div class="grid grid-cols-[1fr_auto] gap-4 items-baseline">
<h2 class="text-2xl font-semibold tracking-tight">
{{ name }}
</h2>

View file

@ -5,8 +5,8 @@
cn(
'grid hover:cursor-pointer gap-4 items-center p-4',
canEdit
? 'grid-cols-[auto,1fr,auto]'
: 'grid-cols-[auto,1fr]'
? 'grid-cols-[auto_1fr_auto]'
: 'grid-cols-[auto_1fr]'
)
"
>

View file

@ -131,7 +131,7 @@
:as="Card"
>
<FormItem
class="grid grid-cols-[1fr,auto] items-center p-6 gap-2"
class="grid grid-cols-[1fr_auto] items-center gap-2"
>
<CardHeader class="space-y-0.5 p-0">
<FormLabel :as="CardTitle">

View file

@ -92,7 +92,7 @@
<div
v-for="(field, index) in value"
:key="index"
class="grid items-center grid-cols-[auto,repeat(3,minmax(0,1fr))] gap-2"
class="grid items-center grid-cols-[auto_repeat(3,minmax(0,1fr))] gap-2"
>
<Button
variant="destructive"
@ -159,7 +159,7 @@
class="block"
>
<FormItem
class="grid grid-cols-[1fr,auto] items-center p-6 gap-2"
class="grid grid-cols-[1fr_auto] items-center gap-2"
>
<CardHeader class="space-y-0.5 p-0">
<FormLabel :as="CardTitle">
@ -187,7 +187,7 @@
class="block"
>
<FormItem
class="grid grid-cols-[1fr,auto] items-center p-6 gap-2"
class="grid grid-cols-[1fr_auto] items-center gap-2"
>
<CardHeader class="space-y-0.5 p-0">
<FormLabel :as="CardTitle">
@ -215,7 +215,7 @@
class="block"
>
<FormItem
class="grid grid-cols-[1fr,auto] items-center p-6 gap-2"
class="grid grid-cols-[1fr_auto] items-center gap-2"
>
<CardHeader class="space-y-0.5 p-0">
<FormLabel :as="CardTitle">

View file

@ -1,5 +1,5 @@
<template>
<Card class="grid grid-cols-[1fr,auto] items-center px-6 py-4 gap-2">
<Card class="grid grid-cols-[1fr_auto] items-center px-6 py-4 gap-2">
<CardHeader class="space-y-0.5 p-0">
<CardTitle class="text-base">
{{ setting.title() }}

View file

@ -1,5 +1,5 @@
<template>
<Card class="grid grid-rows-[1fr,auto] xl:grid-rows-none xl:grid-cols-[1fr,auto] items-center px-6 py-4 gap-4">
<Card class="grid grid-rows-[1fr_auto] xl:grid-rows-none xl:grid-cols-[1fr_auto] items-center px-6 py-4 gap-4">
<CardHeader class="space-y-0.5 p-0">
<CardTitle class="text-base">
{{ setting.title() }}

View file

@ -1,5 +1,5 @@
<template>
<Card class="grid grid-cols-[1fr,auto] items-center px-6 py-4 gap-2">
<Card class="grid grid-cols-[1fr_auto] items-center px-6 py-4 gap-2">
<CardHeader class="space-y-0.5 p-0">
<CardTitle class="text-base">
{{ setting.title() }}

View file

@ -1,5 +1,5 @@
<template>
<Avatar :class="shape.value === 'square' && 'rounded-md'" :size="size">
<Avatar :class="shape.value === 'square' && 'rounded-md'">
<AvatarFallback v-if="name">
{{ getInitials(name) }}
</AvatarFallback>
@ -11,10 +11,9 @@
import { SettingIds } from "~/settings";
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
const { name, size = "base" } = defineProps<{
const { name } = defineProps<{
src?: string;
name?: string;
size?: "base" | "sm" | "lg";
}>();
/**

View file

@ -1,16 +1,16 @@
<template>
<Card
class="flex-row gap-4 p-2"
class="flex-row gap-2 p-2 truncate items-center"
:class="naked ? 'p-0 bg-transparent ring-0 border-none' : ''"
>
<Avatar :src="account.avatar" :name="account.display_name" size="sm" />
<CardContent class="gap-1">
<Avatar :src="account.avatar" :name="account.display_name" class="size-10" />
<CardContent class="leading-tight">
<span
class="truncate font-semibold"
class="font-semibold"
v-render-emojis="account.emojis"
>{{ account.display_name }}</span
>
<span class="truncate text-xs">
<span class="text-xs">
@{{ account.username }}@{{ domain }}
</span>
</CardContent>

View file

@ -0,0 +1,92 @@
<template>
<Dialog>
<DialogTrigger as-child>
<slot />
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Accounts</DialogTitle>
<DialogDescription class="sr-only">
Manage your accounts and settings.
</DialogDescription>
</DialogHeader>
<div v-if="identities.length > 0" class="grid gap-4 py-2">
<div v-for="identity of identities" :key="identity.account.id"
class="grid grid-cols-[1fr_auto] has-[>[data-switch]]:grid-cols-[1fr_auto_auto] gap-2">
<TinyCard :account="identity.account" :domain="identity.instance.domain" naked />
<Button data-switch v-if="currentIdentity?.id !== identity.id"
@click="switchAccount(identity.account.id)" variant="outline">
Switch
</Button>
<Button @click="signOut(appData, identity)" variant="outline" size="icon"
:title="m.sharp_big_mallard_reap()">
<LogOut />
</Button>
</div>
</div>
<div v-else>
<p class="text-sm text-muted-foreground">
Log in to or register an account on your favourite instance.
</p>
</div>
<DialogFooter>
<Button :as="NuxtLink" href="/register" variant="outline">
<UserPlus />
{{ m.honest_few_baboon_pop() }}
</Button>
<Button @click="signInAction">
<LogIn />
{{ m.sunny_pink_hyena_walk() }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
<script lang="ts" setup>
import type { Account } from "@versia/client/types";
import { ChevronsUpDown, LogIn, LogOut, UserPlus } from "lucide-vue-next";
import { toast } from "vue-sonner";
import TinyCard from "~/components/profiles/tiny-card.vue";
import { Button } from "~/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "~/components/ui/dialog";
import * as m from "~/paraglide/messages.js";
import { NuxtLink } from "#components";
import { identity as currentIdentity } from "#imports";
const appData = useAppData();
const signInAction = async () => signIn(appData, await askForInstance());
const switchAccount = async (userId: string) => {
if (userId === currentIdentity.value?.account.id) {
return await navigateTo(`/@${currentIdentity.value.account.username}`);
}
const id = toast.loading("Switching account...");
const identityToSwitch = identities.value.find(
(i) => i.account.id === userId,
);
if (!identityToSwitch) {
toast.dismiss(id);
toast.error("No identity to switch to");
return;
}
currentIdentity.value = identityToSwitch;
toast.dismiss(id);
toast.success("Switched account");
window.location.href = "/";
};
</script>

View file

@ -1,92 +0,0 @@
<script setup lang="ts">
import { BadgeCheck, LogIn, LogOut, UserPlus } from "lucide-vue-next";
import { toast } from "vue-sonner";
import TinyCard from "~/components/profiles/tiny-card.vue";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu";
import * as m from "~/paraglide/messages.js";
import { NuxtLink } from "#components";
const appData = useAppData();
const signInAction = async () => signIn(appData, await askForInstance());
const switchAccount = async (userId: string) => {
if (userId === identity.value?.account.id) {
return await navigateTo(`/@${identity.value.account.username}`);
}
const id = toast.loading("Switching account...");
const identityToSwitch = identities.value.find(
(i) => i.account.id === userId,
);
if (!identityToSwitch) {
toast.dismiss(id);
toast.error("No identity to switch to");
return;
}
identity.value = identityToSwitch;
toast.dismiss(id);
toast.success("Switched account");
window.location.href = "/";
};
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger :as-child="true">
<slot />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel> Your accounts </DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
v-for="identity of identities"
:key="identity.account.id"
@click="switchAccount(identity.account.id)"
:href="`/@${identity.account.username}`"
>
<TinyCard
:account="identity.account"
:domain="identity.instance.domain"
naked
/>
</DropdownMenuItem>
<DropdownMenuItem @click="signInAction">
<UserPlus />
{{ m.sunny_pink_hyena_walk() }}
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator v-if="identity" />
<DropdownMenuGroup v-if="identity">
<DropdownMenuItem
:as="NuxtLink"
:href="`/@${identity.account.username}`"
>
<BadgeCheck />
{{ m.factual_arable_jurgen_endure() }}
</DropdownMenuItem>
<DropdownMenuItem @click="signOut(appData, identity)">
<LogOut />
{{ m.sharp_big_mallard_reap() }}
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuItem v-else :as="NuxtLink" href="/register">
<LogIn />
{{ m.honest_few_baboon_pop() }}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { ChevronsUpDown, DownloadCloud, Pen } from "lucide-vue-next";
import { ChevronsUpDown, DownloadCloud, Pen, UserPlus } from "lucide-vue-next";
import TinyCard from "~/components/profiles/tiny-card.vue";
import { Button } from "~/components/ui/button";
import {
@ -9,7 +9,7 @@ import {
SidebarMenuItem,
} from "~/components/ui/sidebar";
import * as m from "~/paraglide/messages.js";
import AccountSwitcher from "../account/account-switcher.vue";
import AccountManager from "../account/account-manager.vue";
const { $pwa } = useNuxtApp();
</script>
@ -17,38 +17,28 @@ const { $pwa } = useNuxtApp();
<SidebarFooter>
<SidebarMenu class="gap-3">
<SidebarMenuItem>
<AccountSwitcher>
<SidebarMenuButton size="lg">
<TinyCard
v-if="identity"
:account="identity.account"
:domain="identity.instance.domain"
naked
/>
<AccountManager>
<SidebarMenuButton v-if="identity" size="lg">
<TinyCard :account="identity.account" :domain="identity.instance.domain" naked />
<ChevronsUpDown class="ml-auto size-4" />
</SidebarMenuButton>
</AccountSwitcher>
<SidebarMenuButton v-else>
<UserPlus />
{{ m.sunny_pink_hyena_walk() }}
<ChevronsUpDown class="ml-auto size-4" />
</SidebarMenuButton>
</AccountManager>
</SidebarMenuItem>
<SidebarMenuItem class="flex flex-col gap-2">
<Button
v-if="identity"
variant="default"
size="lg"
class="w-full group-data-[collapsible=icon]:px-4"
@click="useEvent('composer:open')"
>
<Button v-if="identity" variant="default" size="lg" class="w-full group-data-[collapsible=icon]:px-4"
@click="useEvent('composer:open')">
<Pen />
<span class="group-data-[collapsible=icon]:hidden">
{{ m.salty_aloof_turkey_nudge() }}
</span>
</Button>
<Button
v-if="$pwa?.needRefresh"
variant="destructive"
size="lg"
class="w-full group-data-[collapsible=icon]:px-4"
@click="$pwa?.updateServiceWorker(true)"
>
<Button v-if="$pwa?.needRefresh" variant="destructive" size="lg"
class="w-full group-data-[collapsible=icon]:px-4" @click="$pwa?.updateServiceWorker(true)">
<DownloadCloud />
<span class="group-data-[collapsible=icon]:hidden">
{{ m.quaint_low_felix_pave() }}

View file

@ -16,7 +16,7 @@ export const alertVariants = cva(
layout: {
default:
"has-[>svg]:grid-cols-[1fr_auto] grid-rows-2 gap-x-3 gap-y-1 items-start",
button: "grid-cols-[auto_1fr_auto] items-center gap-x-3 gap-y-0.5",
button: "grid-cols-[auto_1fr_auto] items-center gap-x-3 gap-y-0.5 *:data-[slot=alert-description]:col-start-2 *:data-[slot=alert-description]:row-start-2 has-[>[data-slot=alert-description]]:[&>button]:row-span-2",
},
},
defaultVariants: {

View file

@ -18,7 +18,7 @@ const delegatedProps = computed(() => {
<AvatarFallback
data-slot="avatar-fallback"
v-bind="delegatedProps"
:class="cn('bg-muted flex size-full items-center justify-center rounded-full', props.class)"
:class="cn('bg-muted flex size-full items-center justify-center', props.class)"
>
<slot />
</AvatarFallback>

View file

@ -9,7 +9,7 @@ const props = defineProps<{
<template>
<div data-slot="card" :class="cn(
'bg-card text-card-foreground flex flex-col gap-6 rounded-md border py-6 shadow-sm',
'bg-card text-card-foreground flex flex-col gap-6 rounded-md border p-6 shadow-sm',
props.class,
)
">

View file

@ -10,7 +10,7 @@ const props = defineProps<{
<template>
<div
data-slot="card-content"
:class="cn('px-6', props.class)"
:class="cn('flex flex-col', props.class)"
>
<slot />
</div>

View file

@ -10,7 +10,7 @@ const props = defineProps<{
<template>
<div
data-slot="card-footer"
:class="cn('flex items-center px-6 [.border-t]:pt-6', props.class)"
:class="cn('flex items-center [.border-t]:pt-6', props.class)"
>
<slot />
</div>

View file

@ -10,7 +10,7 @@ const props = defineProps<{
<template>
<div
data-slot="card-header"
:class="cn('@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6', props.class)"
:class="cn('@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6', props.class)"
>
<slot />
</div>

View file

@ -18,7 +18,7 @@ const delegatedProps = computed(() => {
<DialogOverlay
data-slot="dialog-overlay"
v-bind="delegatedProps"
:class="cn('data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80', props.class)"
:class="cn('data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80 backdrop-blur-md', props.class)"
>
<slot />
</DialogOverlay>

View file

@ -45,9 +45,13 @@ watch(isValidUrl, (value) => {
</script>
<template>
<div class="space-y-3">
<div class="space-y-2">
<Input v-model="modelValue" v-bind="$attrs" />
<p v-if="isValidUrl" class="text-green-600 text-sm"><Check class="inline size-4" /> {{ m.sunny_small_warbler_express() }}</p>
<p v-else-if="(modelValue?.toString().length ?? 0) > 0" class="text-destructive text-sm"><X class="inline size-4" /> {{ m.teal_late_grebe_blend() }}</p>
<p v-if="isValidUrl" class="text-green-600 text-xs">
{{ m.sunny_small_warbler_express() }}
</p>
<p v-else-if="(modelValue?.toString().length ?? 0) > 0" class="text-destructive text-xs">
{{ m.teal_late_grebe_blend() }}
</p>
</div>
</template>

View file

@ -24,7 +24,7 @@ const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
<div
v-if="collapsible === 'none'"
data-slot="sidebar"
:class="cn('bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col', props.class)"
:class="cn('bg-sidebar text-sidebar-foreground flex h-dvh w-(--sidebar-width) flex-col', props.class)"
v-bind="$attrs"
>
<slot />

View file

@ -2,7 +2,9 @@
<SidebarProvider>
<AppSidebar>
<slot v-if="!route.meta.requiresAuth || identity" />
<AuthRequired v-else />
<div class="mx-auto max-w-4xl p-4" v-else>
<AuthRequired />
</div>
</AppSidebar>
</SidebarProvider>
<MobileNavbar v-if="identity" />
@ -27,10 +29,6 @@ const notUsingInput = computed(
activeElement.value?.tagName !== "TEXTAREA" &&
activeElement.value?.contentEditable !== "true",
);
const backgroundImage = useSetting(SettingIds.BackgroundURL);
const canParseUrl = URL.canParse;
const route = useRoute();
watch([n, notUsingInput, d], async () => {

View file

@ -5,18 +5,19 @@
<Transition name="slide-down">
<Alert
v-if="profileEditor?.dirty"
class="grid grid-cols-[1fr_auto] mb-4 absolute top-4 inset-x-4 w-[calc(100%-2rem)]"
layout="button"
class="mb-4 absolute top-4 inset-x-4 w-[calc(100%-2rem)]"
>
<Check class="size-4" />
<AlertTitle>Unsaved changes</AlertTitle>
<AlertDescription class="col-start-1">
<AlertDescription >
Click "apply" to save your changes.
</AlertDescription>
<!-- Add pl-4 because Alert is adding additional padding, which we don't want -->
<Button
variant="secondary"
@click="profileEditor?.submitForm"
class="w-full col-start-2 row-start-1 row-span-2 !pl-4"
class="w-full !pl-4"
>Apply</Button
>
</Alert>

View file

@ -1,6 +1,6 @@
<template>
<div class="md:px-8 px-4 py-2 max-w-7xl mx-auto w-full space-y-6">
<div :class="cn('grid gap-2', canUpload && 'grid-cols-[1fr,auto]')">
<div :class="cn('grid gap-2', canUpload && 'grid-cols-[1fr_auto]')">
<h1
class="scroll-m-20 text-3xl font-extrabold tracking-tight lg:text-4xl capitalize"
>