style: 🎨 Format code with Biome

This commit is contained in:
Jesse Wierzbinski 2025-12-09 22:32:22 +01:00
parent 7ff9d2302a
commit 3627ac0ef8
No known key found for this signature in database
296 changed files with 3257 additions and 2808 deletions

View file

@ -1,15 +1,13 @@
<template>
<TooltipProvider>
<Component is="style">
{{ preferences.custom_css }}
</Component>
<NuxtPwaAssets />
<Component is="style">{{ preferences.custom_css }}</Component>
<NuxtPwaAssets/>
<NuxtLayout>
<NuxtPage />
<NuxtPage/>
</NuxtLayout>
<ConfirmationModal />
<ConfirmationModal/>
<!-- pointer-events-auto fixes https://github.com/unovue/shadcn-vue/issues/462 -->
<Toaster class="pointer-events-auto" />
<Toaster class="pointer-events-auto"/>
</TooltipProvider>
</template>
@ -91,7 +89,10 @@ body {
html.theme-changing * {
/* Stroke and fill aren't animatable */
transition: background-color 1s ease, border 1s ease, color 1s ease,
transition:
background-color 1s ease,
border 1s ease,
color 1s ease,
box-shadow 1s ease !important;
}

View file

@ -1,69 +1,105 @@
<template>
<div v-if="relation" class="overflow-auto max-h-72">
<Note :note="relation.note" :hide-actions="true" :small-layout="true" />
<Note :note="relation.note" :hide-actions="true" :small-layout="true"/>
</div>
<InputGroup class="p-1">
<InputGroupAddon v-if="store.sensitive" align="block-start" class="pt-3">
<Input v-model:model-value="store.contentWarning" placeholder="Put your content warning here" />
<InputGroupAddon
v-if="store.sensitive"
align="block-start"
class="pt-3"
>
<Input
v-model:model-value="store.contentWarning"
placeholder="Put your content warning here"
/>
</InputGroupAddon>
<EditorContent data-slot="input-group-control" @paste-files="uploadFiles" v-model:content="store.content"
v-model:raw-content="store.rawContent" :placeholder="getRandomSplash()"
<EditorContent
data-slot="input-group-control"
@paste-files="uploadFiles"
v-model:content="store.content"
v-model:raw-content="store.rawContent"
:placeholder="getRandomSplash()"
class=" placeholder:text-muted-foreground flex field-sizing-content min-h-58 w-full px-4 text-base disabled:opacity-50 md:text-sm flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none"
:disabled="store.sending" :mode="store.contentType === 'text/html' ? 'rich' : 'plain'" />
:disabled="store.sending"
:mode="store.contentType === 'text/html' ? 'rich' : 'plain'"
/>
<InputGroupAddon v-if="store.files.length > 0" align="block-end" class="overflow-x-auto *:shrink-0">
<Files v-model:files="store.files" :composer-key="composerKey" />
<InputGroupAddon
v-if="store.files.length > 0"
align="block-end"
class="overflow-x-auto *:shrink-0"
>
<Files v-model:files="store.files" :composer-key="composerKey"/>
</InputGroupAddon>
<InputGroupAddon align="block-end">
<Select v-model:model-value="store.contentType">
<SelectTrigger as-child disable-default-classes disable-select-icon>
<SelectTrigger
as-child
disable-default-classes
disable-select-icon
>
<InputGroupButton variant="ghost" size="icon-sm">
<LetterText v-if="store.contentType === 'text/html'" />
<Type v-else />
<LetterText v-if="store.contentType === 'text/html'"/>
<Type v-else/>
</InputGroupButton>
</SelectTrigger>
<SelectContent>
<SelectItem value="text/plain">
Plain text
</SelectItem>
<SelectItem value="text/html">
Rich text
</SelectItem>
<SelectItem value="text/plain">Plain text</SelectItem>
<SelectItem value="text/html">Rich text</SelectItem>
</SelectContent>
</Select>
<VisibilityPicker v-model:visibility="store.visibility">
<InputGroupButton variant="ghost" size="icon-sm" :disabled="store.relation?.type === 'edit'">
<component :is="visibilities[store.visibility].icon" />
<InputGroupButton
variant="ghost"
size="icon-sm"
:disabled="store.relation?.type === 'edit'"
>
<component :is="visibilities[store.visibility].icon"/>
</InputGroupButton>
</VisibilityPicker>
<InputGroupButton variant="ghost" size="icon-sm" @click="fileInput?.click()">
<FilePlus2 />
<InputGroupButton
variant="ghost"
size="icon-sm"
@click="fileInput?.click()"
>
<FilePlus2/>
</InputGroupButton>
<Toggle size="sm" v-model="store.sensitive">
<TriangleAlert />
<TriangleAlert/>
</Toggle>
<InputGroupText :class="['ml-auto', charactersLeft < 0 && 'text-destructive']">
<InputGroupText
:class="['ml-auto', charactersLeft < 0 && 'text-destructive']"
>
{{ charactersLeft.toLocaleString(getLocale(), {
maximumFractionDigits: 2,
notation: 'compact',
compactDisplay: 'short',
}) }}
</InputGroupText>
<Separator orientation="vertical" class="h-4!" />
<InputGroupButton variant="default" size="icon-sm" :disabled="store.sending || !store.canSend"
@click="send">
<Spinner v-if="store.sending" />
<ArrowUp v-else />
<Separator orientation="vertical" class="h-4!"/>
<InputGroupButton
variant="default"
size="icon-sm"
:disabled="store.sending || !store.canSend"
@click="send"
>
<Spinner v-if="store.sending"/>
<ArrowUp v-else/>
<span class="sr-only">Send</span>
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<input type="file" ref="fileInput" @change="uploadFileFromEvent" class="hidden" multiple />
<input
type="file"
ref="fileInput"
@change="uploadFileFromEvent"
class="hidden"
multiple
>
</template>
<script lang="ts" setup>

View file

@ -101,7 +101,7 @@ const relation = ref(
: m.brief_cool_capybara_fear()
}}
</DialogDescription>
<Composer :relation="relation ?? undefined" />
<Composer :relation="relation ?? undefined"/>
</DialogContent>
</Dialog>
</template>

View file

@ -5,26 +5,37 @@
:disabled="file.uploading || file.updating"
class="block bg-card text-card-foreground shadow-sm h-28 overflow-hidden rounded relative min-w-28 *:disabled:opacity-50"
>
<img v-if="file.file?.type.startsWith('image/')" :src="createObjectURL(file.file)" class="object-contain h-28 w-full" :alt="file.alt" />
<FileIcon v-else class="size-6 m-auto text-muted-foreground" />
<img
v-if="file.file?.type.startsWith('image/')"
:src="createObjectURL(file.file)"
class="object-contain h-28 w-full"
:alt="file.alt"
>
<FileIcon v-else class="size-6 m-auto text-muted-foreground"/>
<Badge
v-if="file.file && !(file.uploading || file.updating)"
class="absolute bottom-1 right-1"
variant="default"
>{{ formatBytes(file.file.size) }}</Badge
>
<Spinner v-else-if="file.file" class="absolute bottom-1 right-1 size-8 p-1.5" />
{{ formatBytes(file.file.size) }}
</Badge>
<Spinner
v-else-if="file.file"
class="absolute bottom-1 right-1 size-8 p-1.5"
/>
</DropdownMenuTrigger>
<DropdownMenuContent class="min-w-48">
<DropdownMenuLabel v-if="file.file">{{ file.file.name }}</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuLabel v-if="file.file">
{{ file.file.name }}
</DropdownMenuLabel>
<DropdownMenuSeparator/>
<DropdownMenuItem @click="editCaption">
<Captions />
<Captions/>
Add caption
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuSeparator/>
<DropdownMenuItem @click="emit('remove')">
<Delete />
<Delete/>
Remove
</DropdownMenuItem>
</DropdownMenuContent>

View file

@ -1,6 +1,12 @@
<template>
<FilePreview v-for="(file, index) in files" :key="file.apiId" :file="file" @update:file="files[index] = $event" :composer-key="composerKey"
@remove="files.splice(index, 1)" />
<FilePreview
v-for="(file, index) in files"
:key="file.apiId"
:file="file"
@update:file="files[index] = $event"
:composer-key="composerKey"
@remove="files.splice(index, 1)"
/>
</template>
<script lang="ts" setup>

View file

@ -1,12 +1,19 @@
<template>
<Select v-model:model-value="visibility">
<SelectTrigger as-child disable-default-classes disable-select-icon>
<slot />
<slot/>
</SelectTrigger>
<SelectContent>
<SelectItem v-for="(v, k) in visibilities" :key="k" @click="visibility = k" :value="k">
<div class="flex flex-row gap-3 items-center w-full justify-between">
<component :is="v.icon" class="size-4" />
<SelectItem
v-for="(v, k) in visibilities"
:key="k"
@click="visibility = k"
:value="k"
>
<div
class="flex flex-row gap-3 items-center w-full justify-between"
>
<component :is="v.icon" class="size-4"/>
<div class="flex flex-col gap-1">
<span class="font-semibold">{{ v.name }}</span>
<span>{{ v.text }}</span>

View file

@ -69,30 +69,31 @@ watch(active, (value) => {
<template>
<BubbleMenu :editor="editor">
<ToggleGroup type="multiple"
<ToggleGroup
type="multiple"
v-model="active"
class="bg-popover rounded-md"
class="bg-popover rounded-md"
>
<ToggleGroupItem value="bold">
<BoldIcon />
<BoldIcon/>
</ToggleGroupItem>
<ToggleGroupItem value="italic">
<ItalicIcon />
<ItalicIcon/>
</ToggleGroupItem>
<ToggleGroupItem value="underline">
<UnderlineIcon />
<UnderlineIcon/>
</ToggleGroupItem>
<ToggleGroupItem value="code">
<CurlyBracesIcon />
<CurlyBracesIcon/>
</ToggleGroupItem>
<ToggleGroupItem value="strike">
<StrikethroughIcon />
<StrikethroughIcon/>
</ToggleGroupItem>
<ToggleGroupItem value="subscript">
<SubscriptIcon />
<SubscriptIcon/>
</ToggleGroupItem>
<ToggleGroupItem value="superscript">
<SuperscriptIcon />
<SuperscriptIcon/>
</ToggleGroupItem>
</ToggleGroup>
</BubbleMenu>

View file

@ -1,8 +1,6 @@
<template>
<BubbleMenu :editor="editor" />
<EditorContent :editor="editor"
v-bind="$attrs"
:class="$style.content" />
<BubbleMenu :editor="editor"/>
<EditorContent :editor="editor" v-bind="$attrs" :class="$style.content"/>
</template>
<script lang="ts" setup>
@ -122,7 +120,7 @@ onUnmounted(() => {
@apply font-bold rounded-sm text-primary-foreground bg-primary px-1 py-0.5;
}
.tiptap .emoji>img {
.tiptap .emoji > img {
@apply h-lh align-middle inline hover:scale-110 transition-transform duration-75 ease-in-out;
}
</style>

View file

@ -1,10 +1,23 @@
<template>
<Command class="rounded border shadow-md min-w-[200px] h-fit not-prose" :selected-value="emojis[selectedIndex]?.id">
<Command
class="rounded border shadow-md min-w-[200px] h-fit not-prose"
:selected-value="emojis[selectedIndex]?.id"
>
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup class="emojis-group" heading="Emojis">
<CommandItem :value="emoji.id" v-for="emoji, index in emojis" :key="emoji.id" @click="selectItem(index)" class="scroll-m-10">
<img class="h-[1lh] align-middle inline hover:scale-110 transition-transform duration-75 ease-in-out" :src="emoji.url" :title="emoji.shortcode" />
<CommandItem
:value="emoji.id"
v-for="emoji, index in emojis"
:key="emoji.id"
@click="selectItem(index)"
class="scroll-m-10"
>
<img
class="h-[1lh] align-middle inline hover:scale-110 transition-transform duration-75 ease-in-out"
:src="emoji.url"
:title="emoji.shortcode"
>
<span>{{ emoji.shortcode }}</span>
</CommandItem>
</CommandGroup>

View file

@ -1,11 +1,26 @@
<template>
<Command class="rounded border shadow-md min-w-[200px] h-fit not-prose" :selected-value="items[selectedIndex]?.key">
<Command
class="rounded border shadow-md min-w-[200px] h-fit not-prose"
:selected-value="items[selectedIndex]?.key"
>
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup class="mentions-group" heading="Users">
<CommandItem :value="user.key" v-for="user, index in items" :key="user.key" @click="selectItem(index)" class="scroll-m-10">
<Avatar class="size-4" :src="user.value.avatar" :name="user.value.display_name" />
<span v-render-emojis="user.value.emojis">{{ user.value.display_name }}</span>
<CommandItem
:value="user.key"
v-for="user, index in items"
:key="user.key"
@click="selectItem(index)"
class="scroll-m-10"
>
<Avatar
class="size-4"
:src="user.value.avatar"
:name="user.value.display_name"
/>
<span v-render-emojis="user.value.emojis"
>{{ user.value.display_name }}</span
>
</CommandItem>
</CommandGroup>
</CommandList>

View file

@ -1,9 +1,7 @@
<template>
<Alert>
<AlertTitle>{{ m.fine_arable_lemming_fold() }}</AlertTitle>
<AlertDescription>
{{ m.petty_honest_fish_stir() }}
</AlertDescription>
<AlertDescription>{{ m.petty_honest_fish_stir() }}</AlertDescription>
</Alert>
</template>

View file

@ -1,9 +1,7 @@
<template>
<Alert>
<AlertTitle>{{ m.empty_awful_lark_dart() }}</AlertTitle>
<AlertDescription>
{{ m.clean_even_mayfly_tap() }}
</AlertDescription>
<AlertDescription>{{ m.clean_even_mayfly_tap() }}</AlertDescription>
</Alert>
</template>

View file

@ -1,9 +1,7 @@
<template>
<Alert>
<AlertTitle>{{ m.steep_suave_fish_snap() }}</AlertTitle>
<AlertDescription>
{{ m.muddy_bland_shark_accept() }}
</AlertDescription>
<AlertDescription>{{ m.muddy_bland_shark_accept() }}</AlertDescription>
</Alert>
</template>

View file

@ -3,24 +3,34 @@ import SquarePattern from "../graphics/square-pattern.vue";
</script>
<template>
<div class="grid min-h-screen place-items-center px-6 py-24 sm:py-32 lg:px-8 fixed inset-0 z-[1000000] bg-dark-900">
<SquarePattern />
<div
class="grid min-h-screen place-items-center px-6 py-24 sm:py-32 lg:px-8 fixed inset-0 z-[1000000] bg-dark-900"
>
<SquarePattern/>
<div class="prose prose-invert max-w-lg">
<h1 class="mt-4 text-3xl font-bold tracking-tight text-gray-100 sm:text-5xl">JavaScript is disabled
<h1
class="mt-4 text-3xl font-bold tracking-tight text-gray-100 sm:text-5xl"
>
JavaScript is disabled
</h1>
<p class="mt-6 text-base leading-7 text-gray-400">
This website requires JavaScript to function properly. Please enable JavaScript in your browser
settings.
This website requires JavaScript to function properly. Please
enable JavaScript in your browser settings.
</p>
<p class="mt-6 text-base leading-7 text-gray-400">
If you are using a browser that does not support JavaScript, please consider using a modern browser
like <a href="https://www.mozilla.org/firefox/new/" class="underline">Firefox</a> or <a
href="https://www.google.com/chrome/" class="underline">Chrome</a>.
If you are using a browser that does not support JavaScript,
please consider using a modern browser like
<a href="https://www.mozilla.org/firefox/new/" class="underline"
>Firefox</a
>or <a href="https://www.google.com/chrome/" class="underline"
>Chrome</a
>.
</p>
<p class="mt-6 text-base leading-7 text-gray-400">
This application does not track you, collect user data, use cookies of any kind or send requests to
servers outside of your account's instance.
This application does not track you, collect user data, use
cookies of any kind or send requests to servers outside of your
account's instance.
</p>
</div>
</div>
</template>
</template>

View file

@ -1,18 +1,23 @@
<template>
<Card>
<FormItem class="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-2">
<FormItem
class="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-2"
>
<CardHeader class="flex flex-col gap-1.5 p-0">
<FormLabel class="font-semibold tracking-tight" :as="CardTitle">
{{ title }}
</FormLabel>
<FormDescription class="text-xs leading-none" v-if="description">
<FormDescription
class="text-xs leading-none"
v-if="description"
>
{{ description }}
</FormDescription>
</CardHeader>
<FormControl>
<slot />
<slot/>
</FormControl>
<FormMessage />
<FormMessage/>
</FormItem>
</Card>
</template>

View file

@ -1,15 +1,11 @@
<template>
<FormItem>
<FormLabel>
{{ title }}
</FormLabel>
<FormLabel>{{ title }}</FormLabel>
<FormControl>
<slot />
<slot/>
</FormControl>
<FormDescription v-if="description">
{{ description }}
</FormDescription>
<FormMessage />
<FormDescription v-if="description">{{ description }}</FormDescription>
<FormMessage/>
</FormItem>
</template>

View file

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

View file

@ -1,19 +1,34 @@
<template>
<svg class="absolute inset-x-0 top-0 h-full w-full stroke-primary/[0.07] [mask-image:radial-gradient(100%_100%_at_top_right,var(--primary-foreground),transparent)] pointer-events-none"
aria-hidden="true">
<svg
class="absolute inset-x-0 top-0 h-full w-full stroke-primary/[0.07] [mask-image:radial-gradient(100%_100%_at_top_right,var(--primary-foreground),transparent)] pointer-events-none"
aria-hidden="true"
>
<defs>
<pattern id="983e3e4c-de6d-4c3f-8d64-b9761d1534cc" width="200" height="200" x="50%" y="-1"
patternUnits="userSpaceOnUse">
<pattern
id="983e3e4c-de6d-4c3f-8d64-b9761d1534cc"
width="200"
height="200"
x="50%"
y="-1"
patternUnits="userSpaceOnUse"
>
<path d="M.5 200V.5H200" fill="none"></path>
</pattern>
</defs><svg x="50%" y="-1" class="overflow-visible fill-primary/[0.03]">
<path d="M-200 0h201v201h-201Z M600 0h201v201h-201Z M-400 600h201v201h-201Z M200 800h201v201h-201Z"
stroke-width="0"></path>
</defs>
<svg x="50%" y="-1" class="overflow-visible fill-primary/[0.03]">
<path
d="M-200 0h201v201h-201Z M600 0h201v201h-201Z M-400 600h201v201h-201Z M200 800h201v201h-201Z"
stroke-width="0"
></path>
</svg>
<rect width="100%" height="100%" stroke-width="0" fill="url(#983e3e4c-de6d-4c3f-8d64-b9761d1534cc)"></rect>
<rect
width="100%"
height="100%"
stroke-width="0"
fill="url(#983e3e4c-de6d-4c3f-8d64-b9761d1534cc)"
></rect>
</svg>
</template>
<script lang="ts" setup>
</script>

View file

@ -1,8 +1,11 @@
<template>
<Card class="grid grid-cols-[auto_1fr] gap-2">
<Avatar :src="instance.thumbnail?.url ??
<Avatar
:src="instance.thumbnail?.url ??
'https://cdn.versia.pub/branding/icon.svg'
" :name="instance.title" />
"
:name="instance.title"
/>
<div class="grid text-sm leading-tight *:line-clamp-1">
<span class="truncate font-semibold">
{{
@ -14,7 +17,6 @@
instance.versia_version || instance.version
}}
</span>
</div>
<h1 class="line-clamp-1 text-sm font-semibold col-span-2">
{{

View file

@ -30,7 +30,7 @@ const inputValue = ref<string>("");
<template>
<Dialog>
<DialogTrigger :as-child="true">
<slot />
<slot/>
</DialogTrigger>
<DialogContent class="sm:max-w-[425px]">
<DialogHeader>
@ -40,17 +40,35 @@ const inputValue = ref<string>("");
</DialogDescription>
</DialogHeader>
<div v-if="modalOptions.inputType === 'text'" class="grid gap-4 py-4">
<div
v-if="modalOptions.inputType === 'text'"
class="grid gap-4 py-4"
>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="confirmInput" class="text-right">{{ m.mean_mean_badger_inspire() }}</Label>
<Input id="confirmInput" v-model="inputValue" class="col-span-3" />
<Label for="confirmInput" class="text-right">
{{ m.mean_mean_badger_inspire() }}
</Label>
<Input
id="confirmInput"
v-model="inputValue"
class="col-span-3"
/>
</div>
</div>
<div v-else-if="modalOptions.inputType === 'textarea'" class="grid gap-4 py-4">
<div
v-else-if="modalOptions.inputType === 'textarea'"
class="grid gap-4 py-4"
>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="confirmTextarea" class="text-right">{{ m.mean_mean_badger_inspire() }}</Label>
<Textarea id="confirmTextarea" v-model="inputValue" class="col-span-3" />
<Label for="confirmTextarea" class="text-right">
{{ m.mean_mean_badger_inspire() }}
</Label>
<Textarea
id="confirmTextarea"
v-model="inputValue"
class="col-span-3"
/>
</div>
</div>
@ -58,10 +76,12 @@ const inputValue = ref<string>("");
<Button variant="outline" @click="() => emit('cancel')">
{{ modalOptions.cancelText }}
</Button>
<Button @click="() => emit('confirm', {
<Button
@click="() => emit('confirm', {
confirmed: true,
value: inputValue,
})">
})"
>
{{ modalOptions.confirmText }}
</Button>
</DialogFooter>

View file

@ -75,7 +75,11 @@ const isValid = ref(false);
</script>
<template>
<AlertDialog :key="String(isOpen)" :open="isOpen" @update:open="isOpen = false">
<AlertDialog
:key="String(isOpen)"
:open="isOpen"
@update:open="isOpen = false"
>
<AlertDialogContent class="sm:max-w-[425px] flex flex-col">
<AlertDialogHeader>
<AlertDialogTitle>{{ modalOptions.title }}</AlertDialogTitle>
@ -84,11 +88,23 @@ const isValid = ref(false);
</AlertDialogDescription>
</AlertDialogHeader>
<Input v-if="modalOptions.inputType === 'text'" v-model="inputValue" />
<Input
v-if="modalOptions.inputType === 'text'"
v-model="inputValue"
/>
<UrlInput v-if="modalOptions.inputType === 'url'" v-model="inputValue" placeholder="google.com" v-model:is-valid="isValid" />
<UrlInput
v-if="modalOptions.inputType === 'url'"
v-model="inputValue"
placeholder="google.com"
v-model:is-valid="isValid"
/>
<Textarea v-else-if="modalOptions.inputType === 'textarea'" v-model="inputValue" rows="6" />
<Textarea
v-else-if="modalOptions.inputType === 'textarea'"
v-model="inputValue"
rows="6"
/>
<AlertDialogFooter class="w-full">
<AlertDialogCancel :as-child="true">
@ -97,7 +113,10 @@ const isValid = ref(false);
</Button>
</AlertDialogCancel>
<AlertDialogAction :as-child="true">
<Button @click="handleConfirm" :disabled="!isValid && modalOptions.inputType === 'url'">
<Button
@click="handleConfirm"
:disabled="!isValid && modalOptions.inputType === 'url'"
>
{{ modalOptions.confirmText }}
</Button>
</AlertDialogAction>

View file

@ -1,9 +1,9 @@
<template>
<DrawerContent class="flex flex-col gap-2 px-4 mb-4 [&>:nth-child(2)]:mt-4">
<slot />
<slot/>
</DrawerContent>
</template>
<script lang="ts" setup>
import { DrawerContent } from "../ui/drawer";
</script>
</script>

View file

@ -3,7 +3,7 @@
class="fixed md:hidden bottom-0 inset-x-0 border-t h-16 bg-background z-10 flex items-center justify-around *:h-full *:w-full gap-6 px-4 py-2 [&>a>svg]:size-5 [&>button>svg]:size-5"
>
<Button :as="NuxtLink" href="/" variant="ghost" size="icon">
<Home />
<Home/>
</Button>
<Button
:as="NuxtLink"
@ -11,10 +11,10 @@
variant="ghost"
size="icon"
>
<Bell />
<Bell/>
</Button>
<Button variant="ghost" size="icon">
<User />
<User/>
</Button>
<Button
variant="default"
@ -22,7 +22,7 @@
:title="m.salty_aloof_turkey_nudge()"
@click="useEvent('composer:open')"
>
<Pen />
<Pen/>
</Button>
</div>
</template>

View file

@ -1,9 +1,15 @@
<template>
<Tabs v-model:model-value="current">
<TabsList>
<TabsTrigger v-for="timeline in timelines.filter(
<TabsTrigger
v-for="timeline in timelines.filter(
i => i.requiresLogin ? authStore.isSignedIn : true,
)" :key="timeline.value" :value="timeline.value" :as="NuxtLink" :href="timeline.url">
)"
:key="timeline.value"
:value="timeline.value"
:as="NuxtLink"
:href="timeline.url"
>
{{ timeline.name }}
</TabsTrigger>
</TabsList>

View file

@ -1,7 +1,7 @@
<template>
<Button variant="ghost" class="max-w-14 w-full" size="sm">
<component :is="icon" class="size-4" />
<slot />
<component :is="icon" class="size-4"/>
<slot/>
</Button>
</template>

View file

@ -1,20 +1,58 @@
<template>
<div class="flex flex-row w-full max-w-sm items-stretch justify-between">
<ActionButton :icon="Reply" @click="emit('reply')" :title="m.drab_tense_turtle_comfort()" :disabled="!authStore.isSignedIn">
<ActionButton
:icon="Reply"
@click="emit('reply')"
:title="m.drab_tense_turtle_comfort()"
:disabled="!authStore.isSignedIn"
>
{{ numberFormat(replyCount) }}
</ActionButton>
<ActionButton :icon="Heart" @click="liked ? unlike() : like()" :title="liked ? m.vexed_fluffy_clownfish_dance() : m.royal_close_samuel_scold()" :disabled="!authStore.isSignedIn" :class="liked && '*:fill-red-600 *:text-red-600'">
<ActionButton
:icon="Heart"
@click="liked ? unlike() : like()"
:title="liked ? m.vexed_fluffy_clownfish_dance() : m.royal_close_samuel_scold()"
:disabled="!authStore.isSignedIn"
:class="liked && '*:fill-red-600 *:text-red-600'"
>
{{ numberFormat(likeCount) }}
</ActionButton>
<ActionButton :icon="Repeat" @click="reblogged ? unreblog() : reblog()" :title="reblogged ? m.lime_neat_ox_stab() : m.aware_helpful_marlin_drop()" :disabled="!authStore.isSignedIn" :class="reblogged && '*:text-green-600'">
<ActionButton
:icon="Repeat"
@click="reblogged ? unreblog() : reblog()"
:title="reblogged ? m.lime_neat_ox_stab() : m.aware_helpful_marlin_drop()"
:disabled="!authStore.isSignedIn"
:class="reblogged && '*:text-green-600'"
>
{{ numberFormat(reblogCount) }}
</ActionButton>
<ActionButton :icon="Quote" @click="emit('quote')" :title="m.true_shy_jackal_drip()" :disabled="!authStore.isSignedIn" />
<ActionButton
:icon="Quote"
@click="emit('quote')"
:title="m.true_shy_jackal_drip()"
:disabled="!authStore.isSignedIn"
/>
<Picker @pick="react">
<ActionButton :icon="Smile" :title="m.bald_cool_kangaroo_jump()" :disabled="!authStore.isSignedIn" />
<ActionButton
:icon="Smile"
:title="m.bald_cool_kangaroo_jump()"
:disabled="!authStore.isSignedIn"
/>
</Picker>
<Menu :api-note-string="apiNoteString" :url="url" :remote-url="remoteUrl" :is-remote="isRemote" :author-id="authorId" @edit="emit('edit')" :note-id="noteId" @delete="emit('delete')">
<ActionButton :icon="Ellipsis" :title="m.busy_merry_cowfish_absorb()" />
<Menu
:api-note-string="apiNoteString"
:url="url"
:remote-url="remoteUrl"
:is-remote="isRemote"
:author-id="authorId"
@edit="emit('edit')"
:note-id="noteId"
@delete="emit('delete')"
>
<ActionButton
:icon="Ellipsis"
:title="m.busy_merry_cowfish_absorb()"
/>
</Menu>
</div>
</template>

View file

@ -1,8 +1,17 @@
<template>
<ImageAttachment v-if="attachment.type === 'image'" :attachment="attachment" />
<VideoAttachment v-else-if="attachment.type === 'video' || attachment.type === 'gifv'" :attachment="attachment" />
<AudioAttachment v-else-if="attachment.type === 'audio'" :attachment="attachment" />
<FileAttachment v-else :attachment="attachment" />
<ImageAttachment
v-if="attachment.type === 'image'"
:attachment="attachment"
/>
<VideoAttachment
v-else-if="attachment.type === 'video' || attachment.type === 'gifv'"
:attachment="attachment"
/>
<AudioAttachment
v-else-if="attachment.type === 'audio'"
:attachment="attachment"
/>
<FileAttachment v-else :attachment="attachment"/>
</template>
<script lang="ts" setup>

View file

@ -1,7 +1,13 @@
<template>
<!-- [&:has(>:last-child:nth-child(1))] means "when this element has 1 child" -->
<div class="grid gap-4 grid-cols-2 *:max-h-56 [&:has(>:last-child:nth-child(1))]:grid-cols-1 sm:[&:has(>:last-child:nth-child(1))>*]:max-h-72">
<Attachment v-for="attachment in attachments" :key="attachment.id" :attachment="attachment" />
<div
class="grid gap-4 grid-cols-2 *:max-h-56 [&:has(>:last-child:nth-child(1))]:grid-cols-1 sm:[&:has(>:last-child:nth-child(1))>*]:max-h-72"
>
<Attachment
v-for="attachment in attachments"
:key="attachment.id"
:attachment="attachment"
/>
</div>
</template>

View file

@ -1,16 +1,22 @@
<template>
<Dialog>
<Card class="w-full h-full overflow-hidden relative p-0 *:first:w-full *:first:h-full *:first:object-contain *:first:bg-muted/20">
<Card
class="w-full h-full overflow-hidden relative p-0 *:first:w-full *:first:h-full *:first:object-contain *:first:bg-muted/20"
>
<DialogTrigger v-if="lightbox" :as-child="true">
<slot />
<slot/>
</DialogTrigger>
<slot v-else />
<slot v-else/>
<!-- Alt text viewer -->
<Popover v-if="attachment.description">
<div class="absolute top-0 right-0 p-2">
<PopoverTrigger :as-child="true">
<Button variant="outline" size="icon" title="View alt text">
<Captions />
<Button
variant="outline"
size="icon"
title="View alt text"
>
<Captions/>
</Button>
</PopoverTrigger>
</div>
@ -19,26 +25,42 @@
</PopoverContent>
</Popover>
</Card>
<DialogContent :hide-close="true"
class="duration-200 bg-transparent border-none overflow-hidden !animate-none gap-6 w-screen h-screen !max-w-none">
<DialogContent
:hide-close="true"
class="duration-200 bg-transparent border-none overflow-hidden !animate-none gap-6 w-screen h-screen !max-w-none"
>
<div class="grid grid-rows-[auto_1fr_auto]">
<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="outline" size="icon"
class="ml-auto">
<Download />
<DialogTitle class="sr-only">
{{ attachment.type }}
</DialogTitle>
<Button
as="a"
:href="attachment?.url"
target="_blank"
:download="true"
variant="outline"
size="icon"
class="ml-auto"
>
<Download/>
</Button>
<DialogClose :as-child="true">
<Button variant="outline" size="icon">
<X />
<X/>
</Button>
</DialogClose>
</div>
<div class="flex items-center justify-center overflow-hidden *:max-h-[80vh] *:max-w-[80vw] *:w-full *:h-full *:object-contain">
<slot />
<div
class="flex items-center justify-center overflow-hidden *:max-h-[80vh] *:max-w-[80vw] *:w-full *:h-full *:object-contain"
>
<slot/>
</div>
<DialogDescription class="flex items-center justify-center">
<Card v-if="attachment.description" class="max-w-md max-h-48 overflow-auto text-sm">
<Card
v-if="attachment.description"
class="max-w-md max-h-48 overflow-auto text-sm"
>
<p>{{ attachment.description }}</p>
</Card>
</DialogDescription>

View file

@ -1,13 +1,17 @@
<template>
<Base :attachment="attachment">
<audio :src="attachment.url" :alt="attachment.description ?? undefined" controls />
</Base>
<AttachmentBase :attachment="attachment">
<audio
:src="attachment.url"
:alt="attachment.description ?? undefined"
controls
/>
</AttachmentBase>
</template>
<script lang="ts" setup>
import type { Attachment } from "@versia/client/schemas";
import type { z } from "zod";
import Base from "./base.vue";
import AttachmentBase from "./attachment-base.vue";
const { attachment } = defineProps<{
attachment: z.infer<typeof Attachment>;

View file

@ -1,17 +1,19 @@
<template>
<Base :attachment="attachment" lightbox>
<div class="flex flex-col items-center justify-center min-h-48 text-sm gap-2">
<File class="size-12" />
<AttachmentBase :attachment="attachment" lightbox>
<div
class="flex flex-col items-center justify-center min-h-48 text-sm gap-2"
>
<File class="size-12"/>
<span>File attachment</span>
</div>
</Base>
</AttachmentBase>
</template>
<script lang="ts" setup>
import type { Attachment } from "@versia/client/schemas";
import { File } from "lucide-vue-next";
import type { z } from "zod";
import Base from "./base.vue";
import AttachmentBase from "./attachment-base.vue";
const { attachment } = defineProps<{
attachment: z.infer<typeof Attachment>;

View file

@ -1,13 +1,13 @@
<template>
<Base :attachment="attachment" lightbox>
<img :src="attachment.url" :alt="attachment.description ?? undefined" />
</Base>
<AttachmentBase :attachment="attachment" lightbox>
<img :src="attachment.url" :alt="attachment.description ?? undefined">
</AttachmentBase>
</template>
<script lang="ts" setup>
import type { Attachment } from "@versia/client/schemas";
import type { z } from "zod";
import Base from "./base.vue";
import AttachmentBase from "./attachment-base.vue";
const { attachment } = defineProps<{
attachment: z.infer<typeof Attachment>;

View file

@ -1,13 +1,19 @@
<template>
<Base :attachment="attachment">
<video :src="attachment.url" :alt="attachment.description ?? undefined" controls />
</Base>
<AttachmentBase :attachment="attachment">
<video
:src="attachment.url"
:alt="attachment.description ?? undefined"
controls
>
Your browser does not support the video tag.
</video>
</AttachmentBase>
</template>
<script lang="ts" setup>
import type { Attachment } from "@versia/client/schemas";
import type { z } from "zod";
import Base from "./base.vue";
import AttachmentBase from "./attachment-base.vue";
const { attachment } = defineProps<{
attachment: z.infer<typeof Attachment>;

View file

@ -3,10 +3,18 @@
<p class="text-sm leading-6 wrap-anywhere">
{{ contentWarning || m.sour_seemly_bird_hike() }}
</p>
<Button @click="hidden = !hidden" variant="outline" size="sm" class="col-span-2">
<Button
@click="hidden = !hidden"
variant="outline"
size="sm"
class="col-span-2"
>
{{ hidden ? m.bald_direct_turtle_win() :
m.known_flaky_cockroach_dash() }} {{ characterCount > 0 ? ` (${characterCount} characters` : "" }}{{
attachmentCount > 0 ? `${characterCount > 0 ? " · " : " ("}${attachmentCount} file(s)` : "" }}{{ (characterCount > 0 || attachmentCount > 0) ? ")" : "" }}
m.known_flaky_cockroach_dash() }}
{{ characterCount > 0 ? ` (${characterCount} characters` : "" }}
{{
attachmentCount > 0 ? `${characterCount > 0 ? " · " : " ("}${attachmentCount} file(s)` : "" }}
{{ (characterCount > 0 || attachmentCount > 0) ? ")" : "" }}
</Button>
</div>
</template>

View file

@ -1,14 +1,28 @@
<template>
<ContentWarning v-if="(sensitive || contentWarning) && preferences.show_content_warning" :content-warning="contentWarning" :character-count="characterCount ?? 0" :attachment-count="attachments.length" v-model="hidden" />
<ContentWarning
v-if="(sensitive || contentWarning) && preferences.show_content_warning"
:content-warning="contentWarning"
:character-count="characterCount ?? 0"
:attachment-count="attachments.length"
v-model="hidden"
/>
<OverflowGuard v-if="content" :character-count="characterCount" :class="(hidden && preferences.show_content_warning) && 'hidden'">
<OverflowGuard
v-if="content"
:character-count="characterCount"
:class="(hidden && preferences.show_content_warning) && 'hidden'"
>
<Prose v-html="content" v-render-emojis="emojis"></Prose>
</OverflowGuard>
<Attachments v-if="attachments.length > 0" :attachments="attachments" :class="(hidden && preferences.show_content_warning) && 'hidden'" />
<Attachments
v-if="attachments.length > 0"
:attachments="attachments"
:class="(hidden && preferences.show_content_warning) && 'hidden'"
/>
<div v-if="quote" class="mt-4 rounded border overflow-hidden">
<Note :note="quote" :hide-actions="true" :small-layout="true" />
<Note :note="quote" :hide-actions="true" :small-layout="true"/>
</div>
</template>

View file

@ -1,36 +1,61 @@
<template>
<div class="rounded grid grid-cols-[auto_1fr_auto] items-center gap-3">
<HoverCard v-model:open="popupOpen" @update:open="() => {
<HoverCard
v-model:open="popupOpen"
@update:open="() => {
if (!preferences.popup_avatar_hover) {
popupOpen = false;
}
}" :open-delay="2000">
}"
:open-delay="2000"
>
<HoverCardTrigger :as-child="true">
<NuxtLink :href="urlAsPath" :class="cn('relative size-12', smallLayout && 'size-8')">
<Avatar :class="cn('size-12 border border-card', smallLayout && 'size-8')" :src="author.avatar"
:name="author.display_name" />
<Avatar v-if="cornerAvatar" class="size-6 border absolute -bottom-1 -right-1" :src="cornerAvatar" />
<NuxtLink
:href="urlAsPath"
:class="cn('relative size-12', smallLayout && 'size-8')"
>
<Avatar
:class="cn('size-12 border border-card', smallLayout && 'size-8')"
:src="author.avatar"
:name="author.display_name"
/>
<Avatar
v-if="cornerAvatar"
class="size-6 border absolute -bottom-1 -right-1"
:src="cornerAvatar"
/>
</NuxtLink>
</HoverCardTrigger>
<HoverCardContent class="w-96">
<SmallCard :account="author" />
<SmallCard :account="author"/>
</HoverCardContent>
</HoverCard>
<Col
:class="smallLayout && 'text-sm'">
<Text class="font-semibold" v-render-emojis="author.emojis">{{
<Column :class="smallLayout && 'text-sm'">
<Text class="font-semibold" v-render-emojis="author.emojis">
{{
author.display_name
}}</Text>
}}
</Text>
<div class="-mt-1">
<Address as="span" :username="username" :domain="instance" />
<Address as="span" :username="username" :domain="instance"/>
&middot;
<Text as="span" muted class="ml-auto tracking-normal" :title="fullTime">{{ timeAgo }}</Text>
<Text
as="span"
muted
class="ml-auto tracking-normal"
:title="fullTime"
>
{{ timeAgo }}
</Text>
</div>
</Col>
</Column>
<div v-if="!smallLayout">
<NuxtLink :href="noteUrlAsPath" class="text-xs text-muted-foreground"
:title="visibilities[visibility].text">
<component :is="visibilities[visibility].icon" class="size-4" />
<NuxtLink
:href="noteUrlAsPath"
class="text-xs text-muted-foreground"
:title="visibilities[visibility].text"
>
<component :is="visibilities[visibility].icon" class="size-4"/>
</NuxtLink>
</div>
</div>
@ -49,7 +74,7 @@ import { getLocale } from "~~/paraglide/runtime";
import Address from "../profiles/address.vue";
import Avatar from "../profiles/avatar.vue";
import SmallCard from "../profiles/small-card.vue";
import Col from "../typography/layout/col.vue";
import Column from "../typography/layout/col.vue";
import Text from "../typography/text.vue";
import {
HoverCard,

View file

@ -79,57 +79,71 @@ const _delete = async () => {
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<slot />
<slot/>
</DropdownMenuTrigger>
<DropdownMenuContent class="min-w-56">
<DropdownMenuGroup>
<DropdownMenuItem v-if="authorIsMe" as="button" @click="emit('edit')">
<Pencil />
<DropdownMenuItem
v-if="authorIsMe"
as="button"
@click="emit('edit')"
>
<Pencil/>
{{ m.front_lime_grizzly_persist() }}
</DropdownMenuItem>
<DropdownMenuItem as="button" @click="copyText(apiNoteString)">
<Code />
<Code/>
{{ m.yummy_moving_scallop_sail() }}
</DropdownMenuItem>
<DropdownMenuItem as="button" @click="copyText(noteId)">
<Hash />
<Hash/>
{{ m.sunny_zany_jellyfish_pop() }}
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuSeparator/>
<DropdownMenuGroup>
<DropdownMenuItem as="button" @click="copyText(url)">
<Link />
<Link/>
{{ m.ago_new_pelican_drip() }}
</DropdownMenuItem>
<DropdownMenuItem as="button" v-if="isRemote && remoteUrl" @click="copyText(remoteUrl)">
<Link />
<DropdownMenuItem
as="button"
v-if="isRemote && remoteUrl"
@click="copyText(remoteUrl)"
>
<Link/>
{{ m.solid_witty_zebra_walk() }}
</DropdownMenuItem>
<DropdownMenuItem as="a" v-if="isRemote" target="_blank" rel="noopener noreferrer" :href="remoteUrl">
<ExternalLink />
<DropdownMenuItem
as="a"
v-if="isRemote"
target="_blank"
rel="noopener noreferrer"
:href="remoteUrl"
>
<ExternalLink/>
{{ m.active_trite_lark_inspire() }}
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator v-if="authorIsMe" />
<DropdownMenuSeparator v-if="authorIsMe"/>
<DropdownMenuGroup v-if="authorIsMe">
<DropdownMenuItem as="button" :disabled="true">
<Delete />
<Delete/>
{{ m.real_green_clownfish_pet() }}
</DropdownMenuItem>
<DropdownMenuItem as="button" @click="_delete">
<Trash />
<Trash/>
{{ m.tense_quick_cod_favor() }}
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator v-if="authStore.isSignedIn && !authorIsMe" />
<DropdownMenuSeparator v-if="authStore.isSignedIn && !authorIsMe"/>
<DropdownMenuGroup v-if="authStore.isSignedIn && !authorIsMe">
<DropdownMenuItem as="button" :disabled="true">
<Flag />
<Flag/>
{{ m.great_few_jaguar_rise() }}
</DropdownMenuItem>
<DropdownMenuItem as="button" @click="blockUser(authorId)">
<Ban />
<Ban/>
{{ m.misty_soft_sparrow_vent() }}
</DropdownMenuItem>
</DropdownMenuGroup>

View file

@ -50,7 +50,12 @@
:sensitive="noteToUse.sensitive"
:content-warning="noteToUse.spoiler_text"
/>
<Reactions v-if="noteToUse.reactions && noteToUse.reactions.length > 0" :reactions="noteToUse.reactions" :emojis="noteToUse.emojis" :status-id="noteToUse.id" />
<Reactions
v-if="noteToUse.reactions && noteToUse.reactions.length > 0"
:reactions="noteToUse.reactions"
:emojis="noteToUse.emojis"
:status-id="noteToUse.id"
/>
</CardContent>
<CardFooter v-if="!hideActions">
<Actions

View file

@ -1,18 +1,29 @@
<template>
<div ref="container" class="overflow-y-hidden relative duration-200" :style="{
<div
ref="container"
class="overflow-y-hidden relative duration-200"
:style="{
maxHeight: collapsed ? '18rem' : `${container?.scrollHeight}px`,
}">
<slot />
<div v-if="isOverflowing && collapsed"
class="absolute inset-x-0 bottom-0 h-36 bg-gradient-to-t from-black/5 to-transparent rounded-b"></div>
<Button v-if="isOverflowing" @click="collapsed = !collapsed"
class="absolute bottom-2 right-1/2 translate-x-1/2">{{
}"
>
<slot/>
<div
v-if="isOverflowing && collapsed"
class="absolute inset-x-0 bottom-0 h-36 bg-gradient-to-t from-black/5 to-transparent rounded-b"
></div>
<Button
v-if="isOverflowing"
@click="collapsed = !collapsed"
class="absolute bottom-2 right-1/2 translate-x-1/2"
>
{{
collapsed
? `${m.lazy_honest_mammoth_bump()}${formattedCharacterCount ? `${m.dark_spare_goldfish_charm({
count: formattedCharacterCount,
})}` : ""}`
: m.that_misty_mule_arrive()
}}</Button>
}}
</Button>
</div>
</template>

View file

@ -1,9 +1,11 @@
<template>
<div :class="[
<div
:class="[
'prose prose-sm block relative dark:prose-invert duration-200 !max-w-full break-words prose-a:no-underline hover:prose-a:underline',
$style.content,
]">
<slot />
]"
>
<slot/>
</div>
</template>

View file

@ -1,6 +1,12 @@
<template>
<div class="flex flex-row gap-2 flex-wrap">
<Reaction v-for="reaction in reactions" :key="reaction.name" :reaction="reaction" :emoji="emojis.find(e => `:${e.shortcode}:` === reaction.name)" :status-id="statusId" />
<Reaction
v-for="reaction in reactions"
:key="reaction.name"
:reaction="reaction"
:emoji="emojis.find(e => `:${e.shortcode}:` === reaction.name)"
:status-id="statusId"
/>
</div>
</template>

View file

@ -1,8 +1,6 @@
<template>
<div class="sticky top-2 z-10 flex items-center justify-center p-2">
<Badge variant="secondary">
{{ categoryName }}
</Badge>
<Badge variant="secondary">{{ categoryName }}</Badge>
</div>
</template>

View file

@ -1,8 +1,17 @@
<template>
<div class="p-2 text-sm font-semibold border-0 rounded-none text-center flex flex-row items-center gap-2 truncate">
<img v-if="(emoji as InferredEmoji)?.url" :src="(emoji as InferredEmoji)?.url"
:alt="(emoji as InferredEmoji)?.shortcode" class="h-8 align-middle inline not-prose" />
<span v-else-if="(emoji as UnicodeEmoji)?.unicode" class="text-2xl align-middle inline not-prose">
<div
class="p-2 text-sm font-semibold border-0 rounded-none text-center flex flex-row items-center gap-2 truncate"
>
<img
v-if="(emoji as InferredEmoji)?.url"
:src="(emoji as InferredEmoji)?.url"
:alt="(emoji as InferredEmoji)?.shortcode"
class="h-8 align-middle inline not-prose"
>
<span
v-else-if="(emoji as UnicodeEmoji)?.unicode"
class="text-2xl align-middle inline not-prose"
>
{{ (emoji as UnicodeEmoji)?.unicode }}
</span>
{{ (emoji as InferredEmoji)?.shortcode || (emoji as UnicodeEmoji)?.shortcode }}

View file

@ -1,9 +1,22 @@
<template>
<Button @focus="() => emit('select', emoji)" @mouseenter="() => emit('select', emoji)" @click="() => emit('pick', emoji)" size="icon" variant="ghost"
class="size-12">
<img v-if="(emoji as InferredEmoji).url" :src="(emoji as InferredEmoji).url"
:alt="(emoji as InferredEmoji).shortcode" class="h-8 align-middle inline not-prose" />
<span v-else-if="(emoji as UnicodeEmoji).unicode" class="text-2xl align-middle inline not-prose">
<Button
@focus="() => emit('select', emoji)"
@mouseenter="() => emit('select', emoji)"
@click="() => emit('pick', emoji)"
size="icon"
variant="ghost"
class="size-12"
>
<img
v-if="(emoji as InferredEmoji).url"
:src="(emoji as InferredEmoji).url"
:alt="(emoji as InferredEmoji).shortcode"
class="h-8 align-middle inline not-prose"
>
<span
v-else-if="(emoji as UnicodeEmoji).unicode"
class="text-2xl align-middle inline not-prose"
>
{{ (emoji as UnicodeEmoji).unicode }}
</span>
</Button>

View file

@ -1,31 +1,57 @@
<template>
<Popover v-model:open="open">
<PopoverTrigger as-child>
<slot />
<slot/>
</PopoverTrigger>
<PopoverContent class="p-0 w-fit">
<div class="grid-cols-[minmax(0,1fr)_auto] gap-0 grid divide-x *:h-112 *:overflow-y-auto"
orientation="vertical">
<div class="grid grid-rows-[auto_minmax(0,1fr)_auto] gap-0" ref="emojiContainer">
<div
class="grid-cols-[minmax(0,1fr)_auto] gap-0 grid divide-x *:h-112 *:overflow-y-auto"
orientation="vertical"
>
<div
class="grid grid-rows-[auto_minmax(0,1fr)_auto] gap-0"
ref="emojiContainer"
>
<div class="p-2">
<Input placeholder="Search" v-model="filter" />
<Input placeholder="Search" v-model="filter"/>
</div>
<VList :data="virtualizedItems" #default="{ item }" class="relative" :style="{
<VList
:data="virtualizedItems"
#default="{ item }"
class="relative"
:style="{
width: `calc(var(--spacing) * ((12 * ${EMOJI_PER_ROW}) + (${EMOJI_PER_ROW} - 1)) + var(--spacing) * 4)`,
}">
<CategoryHeader :key="item.headerId" v-if="item.type === 'header'" :category-name="item.name" />
<div v-else-if="item.type === 'emoji-row'" :key="item.rowId" class="flex gap-1 p-2">
<Emoji v-for="emoji in item.emojis" :key="getEmojiKey(emoji)" :emoji="emoji"
@select="(e) => selectedEmoji = e" @pick="e => {
}"
>
<CategoryHeader
:key="item.headerId"
v-if="item.type === 'header'"
:category-name="item.name"
/>
<div
v-else-if="item.type === 'emoji-row'"
:key="item.rowId"
class="flex gap-1 p-2"
>
<Emoji
v-for="emoji in item.emojis"
:key="getEmojiKey(emoji)"
:emoji="emoji"
@select="(e) => selectedEmoji = e"
@pick="e => {
emit('pick', e); open = false;
}" />
}"
/>
</div>
</VList>
<EmojiDisplay :emoji="selectedEmoji" :style="{
<EmojiDisplay
:emoji="selectedEmoji"
:style="{
width: `calc(var(--spacing) * ((12 * ${EMOJI_PER_ROW}) + (${EMOJI_PER_ROW} - 1)) + var(--spacing) * 4)`,
}" />
}"
/>
</div>
<Sidebar :categories="categories" @select="scrollToCategory" />
<Sidebar :categories="categories" @select="scrollToCategory"/>
</div>
</PopoverContent>
</Popover>

View file

@ -1,8 +1,23 @@
<template>
<div class="grid gap-1 bg-transparent p-2">
<Button v-for="category in categories" :key="category.name" size="icon" variant="ghost" @click="() => emit('select', category)">
<component v-if="category.groupId" :is="emojiGroupIconMap[category.groupId]" class="size-6 text-primary" />
<img v-else-if="category.src" :src="category.src" class="size-6 align-middle inline not-prose" role="presentation" />
<Button
v-for="category in categories"
:key="category.name"
size="icon"
variant="ghost"
@click="() => emit('select', category)"
>
<component
v-if="category.groupId"
:is="emojiGroupIconMap[category.groupId]"
class="size-6 text-primary"
/>
<img
v-else-if="category.src"
:src="category.src"
class="size-6 align-middle inline not-prose"
role="presentation"
>
</Button>
</div>
</template>

View file

@ -1,23 +1,38 @@
<template>
<HoverCard @update:open="(open) => open && accounts === null && refreshReactions()">
<HoverCard
@update:open="(open) => open && accounts === null && refreshReactions()"
>
<HoverCardTrigger as-child>
<Button @click="reaction.me ? !reaction.remote && unreact() : !reaction.remote && react()" :variant="reaction.me ? 'secondary' : reaction.remote ? 'ghost' : 'outline'" size="sm" class="gap-2">
<img v-if="emoji" :src="emoji.url" :alt="emoji.shortcode"
class="h-[1lh] align-middle inline not-prose" />
<span v-else>
{{ reaction.name }}
</span>
<Button
@click="reaction.me ? !reaction.remote && unreact() : !reaction.remote && react()"
:variant="reaction.me ? 'secondary' : reaction.remote ? 'ghost' : 'outline'"
size="sm"
class="gap-2"
>
<img
v-if="emoji"
:src="emoji.url"
:alt="emoji.shortcode"
class="h-[1lh] align-middle inline not-prose"
>
<span v-else> {{ reaction.name }}</span>
{{ formatNumber(reaction.count) }}
</Button>
</HoverCardTrigger>
<HoverCardContent class="p-3">
<Spinner v-if="accounts === null" class="border-0" />
<Spinner v-if="accounts === null" class="border-0"/>
<ul v-else class="flex flex-col gap-4">
<li
v-for="account in accounts">
<NuxtLink :to="`/@${account.acct}`" class="flex items-center gap-2">
<Avatar class="size-6" :key="account.id" :src="account.avatar"
:name="account.display_name || account.username" />
<li v-for="account in accounts">
<NuxtLink
:to="`/@${account.acct}`"
class="flex items-center gap-2"
>
<Avatar
class="size-6"
:key="account.id"
:src="account.avatar"
:name="account.display_name || account.username"
/>
<span class="text-sm font-semibold line-clamp-1">
{{ account.display_name || account.username }}
</span>

View file

@ -1,9 +1,13 @@
<template>
<NuxtLink :href="urlAsPath">
<Card class="flex-row px-2 py-1 items-center gap-2 hover:bg-muted duration-100 text-sm">
<Repeat class="size-4 text-primary" />
<Avatar class="size-6 border" :src="avatar" :name="displayName" />
<span class="font-semibold" v-render-emojis="emojis">{{ displayName }}</span>
<Card
class="flex-row px-2 py-1 items-center gap-2 hover:bg-muted duration-100 text-sm"
>
<Repeat class="size-4 text-primary"/>
<Avatar class="size-6 border" :src="avatar" :name="displayName"/>
<span class="font-semibold" v-render-emojis="emojis"
>{{ displayName }}</span
>
{{ m.large_vivid_horse_catch() }}
</Card>
</NuxtLink>

View file

@ -1,29 +1,55 @@
<template>
<div v-if="relationship?.requested_by !== false" class="flex flex-row items-center gap-3 p-4">
<div
v-if="relationship?.requested_by !== false"
class="flex flex-row items-center gap-3 p-4"
>
<NuxtLink :href="followerUrl" class="relative size-10">
<Avatar class="size-10 border border-border" :src="follower.avatar" :name="follower.display_name" />
<Avatar
class="size-10 border border-border"
:src="follower.avatar"
:name="follower.display_name"
/>
</NuxtLink>
<div class="flex flex-col gap-0.5 justify-center flex-1 text-left leading-tight text-sm">
<span class="truncate font-semibold" v-render-emojis="follower.emojis">{{
<div
class="flex flex-col gap-0.5 justify-center flex-1 text-left leading-tight text-sm"
>
<span
class="truncate font-semibold"
v-render-emojis="follower.emojis"
>{{
follower.display_name
}}</span>
}}</span
>
<span class="truncate tracking-tight">
<Address :username="username" :domain="domain" />
<Address :username="username" :domain="domain"/>
</span>
</div>
</div>
<div v-if="loading" class="flex p-2 items-center justify-center h-12">
<Loader class="size-4 animate-spin" />
<Loader class="size-4 animate-spin"/>
</div>
<div v-else-if="relationship?.requested_by === false" class="flex p-2 items-center justify-center h-12">
<Check class="size-4" />
<div
v-else-if="relationship?.requested_by === false"
class="flex p-2 items-center justify-center h-12"
>
<Check class="size-4"/>
</div>
<div v-else class="grid grid-cols-2 p-2 gap-2">
<Button variant="secondary" size="sm" @click="accept" :title="m.slow_these_kestrel_sail()">
<Check />
<Button
variant="secondary"
size="sm"
@click="accept"
:title="m.slow_these_kestrel_sail()"
>
<Check/>
</Button>
<Button variant="ghost" size="sm" @click="reject" :title="m.weary_steep_yak_embrace()">
<X />
<Button
variant="ghost"
size="sm"
@click="reject"
:title="m.weary_steep_yak_embrace()"
>
<X/>
</Button>
</div>
</template>

View file

@ -7,7 +7,7 @@
v-if="notification.account"
class="flex flex-row items-center gap-2 px-2"
>
<component :is="icon" class="size-5 shrink-0" />
<component :is="icon" class="size-5 shrink-0"/>
<Avatar
class="size-5 border border-card"
:src="notification.account.avatar"
@ -25,7 +25,7 @@
class="ml-auto [&_svg]:data-[state=open]:-rotate-180"
:title="open ? 'Collapse' : 'Expand'"
>
<ChevronDown class="duration-200" />
<ChevronDown class="duration-200"/>
</Button>
</CollapsibleTrigger>
</CardHeader>

View file

@ -1,17 +1,43 @@
<template>
<section class="space-y-2">
<CardTitle class="text-xs">
{{ name }}
</CardTitle>
<CardTitle class="text-xs">{{ name }}</CardTitle>
<Card class="p-0 gap-0">
<div v-for="preference of preferences" :key="preference">
<TextPreferenceVue v-if="(prefs[preference] instanceof TextPreference)" :pref="(prefs[preference] as TextPreference)" :name="preference" />
<BooleanPreferenceVue v-else-if="(prefs[preference] instanceof BooleanPreference)" :pref="(prefs[preference] as BooleanPreference)" :name="preference" />
<SelectPreferenceVue v-else-if="(prefs[preference] instanceof SelectPreference)" :pref="(prefs[preference] as SelectPreference<string>)" :name="preference" />
<NumberPreferenceVue v-else-if="(prefs[preference] instanceof NumberPreference)" :pref="(prefs[preference] as NumberPreference)" :name="preference" />
<MultiSelectPreferenceVue v-else-if="(prefs[preference] instanceof MultiSelectPreference)" :pref="(prefs[preference] as MultiSelectPreference<string>)" :name="preference" />
<CodePreferenceVue v-else-if="(prefs[preference] instanceof CodePreference)" :pref="(prefs[preference] as CodePreference)" :name="preference" />
<UrlPreferenceVue v-else-if="(prefs[preference] instanceof UrlPreference)" :pref="(prefs[preference] as UrlPreference)" :name="preference" />
<TextPreferenceVue
v-if="(prefs[preference] instanceof TextPreference)"
:pref="(prefs[preference] as TextPreference)"
:name="preference"
/>
<BooleanPreferenceVue
v-else-if="(prefs[preference] instanceof BooleanPreference)"
:pref="(prefs[preference] as BooleanPreference)"
:name="preference"
/>
<SelectPreferenceVue
v-else-if="(prefs[preference] instanceof SelectPreference)"
:pref="(prefs[preference] as SelectPreference<string>)"
:name="preference"
/>
<NumberPreferenceVue
v-else-if="(prefs[preference] instanceof NumberPreference)"
:pref="(prefs[preference] as NumberPreference)"
:name="preference"
/>
<MultiSelectPreferenceVue
v-else-if="(prefs[preference] instanceof MultiSelectPreference)"
:pref="(prefs[preference] as MultiSelectPreference<string>)"
:name="preference"
/>
<CodePreferenceVue
v-else-if="(prefs[preference] instanceof CodePreference)"
:pref="(prefs[preference] as CodePreference)"
:name="preference"
/>
<UrlPreferenceVue
v-else-if="(prefs[preference] instanceof UrlPreference)"
:pref="(prefs[preference] as UrlPreference)"
:name="preference"
/>
</div>
</Card>
</section>

View file

@ -1,13 +1,17 @@
<template>
<Card class="grid gap-3 text-sm">
<dl class="grid gap-3">
<div v-for="[key, value] of data" :key="key" class="flex flex-row items-baseline justify-between gap-4 truncate">
<dt class="text-muted-foreground">
{{ key }}
</dt>
<dd class="font-mono" v-if="typeof value === 'string'">{{ value }}</dd>
<div
v-for="[key, value] of data"
:key="key"
class="flex flex-row items-baseline justify-between gap-4 truncate"
>
<dt class="text-muted-foreground">{{ key }}</dt>
<dd class="font-mono" v-if="typeof value === 'string'">
{{ value }}
</dd>
<dd class="font-mono" v-else>
<component :is="value" />
<component :is="value"/>
</dd>
</div>
</dl>

View file

@ -77,46 +77,70 @@ useListen("preferences:open", () => {
<template>
<Dialog v-model:open="open" v-if="authStore.isSignedIn">
<DialogContent class="md:max-w-5xl w-full h-full p-0 md:max-h-[70dvh] overflow-hidden">
<Tabs class="md:grid-cols-[auto_minmax(0,1fr)] !grid gap-2 *:p-4 overflow-hidden *:overflow-y-auto *:h-full" orientation="vertical"
:default-value="pages[0]">
<DialogHeader class="gap-6 grid grid-rows-[auto_minmax(0,1fr)] border-b md:border-b-0 md:border-r min-w-60 text-left">
<div class="grid gap-3 items-center grid-cols-[auto_minmax(0,1fr)]">
<Avatar :name="authStore.account!.display_name || authStore.account!.username"
:src="authStore.account!.avatar" />
<DialogContent
class="md:max-w-5xl w-full h-full p-0 md:max-h-[70dvh] overflow-hidden"
>
<Tabs
class="md:grid-cols-[auto_minmax(0,1fr)] !grid gap-2 *:p-4 overflow-hidden *:overflow-y-auto *:h-full"
orientation="vertical"
:default-value="pages[0]"
>
<DialogHeader
class="gap-6 grid grid-rows-[auto_minmax(0,1fr)] border-b md:border-b-0 md:border-r min-w-60 text-left"
>
<div
class="grid gap-3 items-center grid-cols-[auto_minmax(0,1fr)]"
>
<Avatar
:name="authStore.account!.display_name || authStore.account!.username"
:src="authStore.account!.avatar"
/>
<DialogTitle>Preferences</DialogTitle>
</div>
<DialogDescription class="sr-only">
Make changes to your preferences here.
</DialogDescription>
<TabsList class="md:grid md:grid-cols-1 w-full h-fit *:justify-start !justify-start">
<TabsTrigger v-for="page in pages" :key="page" :value="page">
<component :is="icons[page]" class="size-4 mr-2" />
<TabsList
class="md:grid md:grid-cols-1 w-full h-fit *:justify-start !justify-start"
>
<TabsTrigger
v-for="page in pages"
:key="page"
:value="page"
>
<component :is="icons[page]" class="size-4 mr-2"/>
{{ page }}
</TabsTrigger>
</TabsList>
</DialogHeader>
<TabsContent v-for="page in pages.filter(p => !extraPages.includes(p))" :key="page" :value="page"
as-child>
<TabsContent
v-for="page in pages.filter(p => !extraPages.includes(p))"
:key="page"
:value="page"
as-child
>
<Page :title="page">
<Category v-for="category in categories[page]" :key="category"
<Category
v-for="category in categories[page]"
:key="category"
:preferences="Object.entries(preferences).filter(([, p]) => p.options.category === `${page}/${category}`).map(([k,]) => k as keyof typeof preferences)"
:name="category" />
:name="category"
/>
</Page>
</TabsContent>
<TabsContent value="Emojis" as-child>
<Page title="Emojis">
<Emojis />
<Emojis/>
</Page>
</TabsContent>
<TabsContent value="Account" as-child>
<Page title="Account">
<Profile />
<Profile/>
</Page>
</TabsContent>
<TabsContent value="Developer" as-child>
<Page title="Developer">
<Developer />
<Developer/>
</Page>
</TabsContent>
<TabsContent value="About" as-child>
@ -126,25 +150,53 @@ useListen("preferences:open", () => {
{{ pkg.description }}
</p>
<Stats />
<Stats/>
</section>
<Separator />
<Separator/>
<section class="space-y-2">
<h3 class="text-lg font-semibold tracking-tight">Developers</h3>
<div class="grid lg:grid-cols-3 md:grid-cols-2 grid-cols-1 gap-4">
<TinyCard v-if="author1" :account="author1" domain="vs.cpluspatch.com" />
<TinyCard v-if="author2" :account="author2" domain="social.lysand.org" />
<TinyCard v-if="author3" :account="author3" domain="social.lysand.org" />
<TinyCard v-if="author4" :account="author4" domain="v.everypizza.im" />
<h3 class="text-lg font-semibold tracking-tight">
Developers
</h3>
<div
class="grid lg:grid-cols-3 md:grid-cols-2 grid-cols-1 gap-4"
>
<TinyCard
v-if="author1"
:account="author1"
domain="vs.cpluspatch.com"
/>
<TinyCard
v-if="author2"
:account="author2"
domain="social.lysand.org"
/>
<TinyCard
v-if="author3"
:account="author3"
domain="social.lysand.org"
/>
<TinyCard
v-if="author4"
:account="author4"
domain="v.everypizza.im"
/>
</div>
</section>
<Separator />
<Separator/>
<section class="space-y-2">
<h3 class="text-lg font-semibold tracking-tight">Dependencies</h3>
<ul class="grid lg:grid-cols-2 gap-2 grid-cols-1 items-center justify-center list-disc ml-6">
<li v-for="[dep, version] in Object.entries(pkg.dependencies)" :key="dep">
<h3 class="text-lg font-semibold tracking-tight">
Dependencies
</h3>
<ul
class="grid lg:grid-cols-2 gap-2 grid-cols-1 items-center justify-center list-disc ml-6"
>
<li
v-for="[dep, version] in Object.entries(pkg.dependencies)"
:key="dep"
>
<code
class="rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs font-semibold">
class="rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs font-semibold"
>
{{ dep }}@{{ version }}
</code>
</li>

View file

@ -1,11 +1,11 @@
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<slot />
<slot/>
</DropdownMenuTrigger>
<DropdownMenuContent class="min-w-48">
<DropdownMenuContent class="min-w-48">
<DropdownMenuItem @click="deleteAll" :disabled="!canEdit">
<Delete />
<Delete/>
{{ m.tense_quick_cod_favor() }}
</DropdownMenuItem>
</DropdownMenuContent>

View file

@ -1,13 +1,18 @@
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="ghost" size="icon" title="Open menu" class="size-8 p-0">
<MoreHorizontal class="size-4" />
<Button
variant="ghost"
size="icon"
title="Open menu"
class="size-8 p-0"
>
<MoreHorizontal class="size-4"/>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent class="min-w-48">
<DropdownMenuContent class="min-w-48">
<DropdownMenuItem @click="editName">
<TextCursorInput />
<TextCursorInput/>
{{ m.cuddly_such_swallow_hush() }}
</DropdownMenuItem>
<!-- <DropdownMenuItem @click="editCaption">
@ -16,7 +21,7 @@
</DropdownMenuItem>
<DropdownMenuSeparator /> -->
<DropdownMenuItem @click="_delete">
<Delete />
<Delete/>
{{ m.tense_quick_cod_favor() }}
</DropdownMenuItem>
</DropdownMenuContent>

View file

@ -1,7 +1,7 @@
<template>
<div v-if="authStore.emojis.length > 0" class="grow">
<Table :emojis="authStore.emojis" :can-upload="canUpload" />
</div>
<div v-if="authStore.emojis.length > 0" class="grow">
<Table :emojis="authStore.emojis" :can-upload="canUpload"/>
</div>
</template>
<script lang="ts" setup>

View file

@ -282,27 +282,34 @@ const table = useVueTable({
<template>
<div class="w-full">
<div class="flex gap-2 items-center py-4">
<Input class="max-w-52 mr-auto" placeholder="Filter emojis..."
<Input
class="max-w-52 mr-auto"
placeholder="Filter emojis..."
:model-value="(table.getColumn('shortcode')?.getFilterValue() as string)"
@update:model-value="table.getColumn('shortcode')?.setFilterValue($event)" />
@update:model-value="table.getColumn('shortcode')?.setFilterValue($event)"
/>
<Uploader v-if="props.canUpload">
<Button variant="outline" size="icon" title="Upload emoji">
<Plus class="size-4" />
<Plus class="size-4"/>
</Button>
</Uploader>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="outline">
Columns
<ChevronDown class="ml-2 size-4" />
<ChevronDown class="ml-2 size-4"/>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuCheckboxItem
v-for="column in table.getAllColumns().filter((column) => column.getCanHide())" :key="column.id"
class="capitalize" :model-value="column.getIsVisible()" @update:model-value="(value) => {
v-for="column in table.getAllColumns().filter((column) => column.getCanHide())"
:key="column.id"
class="capitalize"
:model-value="column.getIsVisible()"
@update:model-value="(value) => {
column.toggleVisibility(!!value)
}">
}"
>
{{ column.id }}
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
@ -311,19 +318,40 @@ const table = useVueTable({
<div class="rounded-md border">
<Table>
<TableHeader>
<TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
<TableHead v-for="header in headerGroup.headers" :key="header.id" class="">
<FlexRender v-if="!header.isPlaceholder" :render="header.column.columnDef.header"
:props="header.getContext()" />
<TableRow
v-for="headerGroup in table.getHeaderGroups()"
:key="headerGroup.id"
>
<TableHead
v-for="header in headerGroup.headers"
:key="header.id"
class=""
>
<FlexRender
v-if="!header.isPlaceholder"
:render="header.column.columnDef.header"
:props="header.getContext()"
/>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<template v-if="table.getRowModel().rows?.length">
<template v-for="row in table.getRowModel().rows" :key="row.id">
<TableRow :data-state="row.getIsSelected() && 'selected'">
<TableCell v-for="cell in row.getVisibleCells()" :key="cell.id">
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
<template
v-for="row in table.getRowModel().rows"
:key="row.id"
>
<TableRow
:data-state="row.getIsSelected() && 'selected'"
>
<TableCell
v-for="cell in row.getVisibleCells()"
:key="cell.id"
>
<FlexRender
:render="cell.column.columnDef.cell"
:props="cell.getContext()"
/>
</TableCell>
</TableRow>
<TableRow v-if="row.getIsExpanded()">
@ -335,7 +363,10 @@ const table = useVueTable({
</template>
<TableRow v-else>
<TableCell :colspan="columns.length" class="h-24 text-center">
<TableCell
:colspan="columns.length"
class="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
@ -345,15 +376,24 @@ const table = useVueTable({
<div class="flex items-center justify-end space-x-2 py-4">
<div class="flex-1 text-sm text-muted-foreground">
{{ table.getFilteredSelectedRowModel().rows.length }} of
{{ table.getFilteredRowModel().rows.length }} row(s) selected.
{{ table.getFilteredSelectedRowModel().rows.length }}of
{{ table.getFilteredRowModel().rows.length }}row(s) selected.
</div>
<div class="space-x-2">
<Button variant="outline" size="sm" :disabled="!table.getCanPreviousPage()"
@click="table.previousPage()">
<Button
variant="outline"
size="sm"
:disabled="!table.getCanPreviousPage()"
@click="table.previousPage()"
>
Previous
</Button>
<Button variant="outline" size="sm" :disabled="!table.getCanNextPage()" @click="table.nextPage()">
<Button
variant="outline"
size="sm"
:disabled="!table.getCanNextPage()"
@click="table.nextPage()"
>
Next
</Button>
</div>

View file

@ -1,12 +1,10 @@
<template>
<Dialog v-model:open="open">
<DialogTrigger>
<slot />
<slot/>
</DialogTrigger>
<DialogContent>
<DialogTitle>
{{ m.whole_icy_puffin_smile() }}
</DialogTitle>
<DialogTitle>{{ m.whole_icy_puffin_smile() }}</DialogTitle>
<DialogDescription class="sr-only">
{{ m.frail_great_marten_pet() }}
</DialogDescription>
@ -20,28 +18,28 @@
class="h-full object-cover"
:src="createObjectURL(values.image as File)"
:alt="values.alt"
/>
>
</div>
<div class="bg-zinc-700">
<img
class="h-full object-cover"
:src="createObjectURL(values.image as File)"
:alt="values.alt"
/>
>
</div>
<div class="bg-zinc-400">
<img
class="h-full object-cover"
:src="createObjectURL(values.image as File)"
:alt="values.alt"
/>
>
</div>
<div class="bg-foreground">
<img
class="h-full object-cover"
:src="createObjectURL(values.image as File)"
:alt="values.alt"
/>
>
</div>
</div>
@ -68,15 +66,13 @@
<FormDescription>
{{ m.lime_late_millipede_urge() }}
</FormDescription>
<FormMessage />
<FormMessage/>
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="shortcode">
<FormItem>
<FormLabel>
{{ m.happy_mild_fox_gleam() }}
</FormLabel>
<FormLabel>{{ m.happy_mild_fox_gleam() }}</FormLabel>
<FormControl>
<Input
v-bind="componentField"
@ -86,7 +82,7 @@
<FormDescription>
{{ m.glad_day_kestrel_amaze() }}
</FormDescription>
<FormMessage />
<FormMessage/>
</FormItem>
</FormField>
@ -101,7 +97,7 @@
:disabled="isSubmitting"
/>
</FormControl>
<FormMessage />
<FormMessage/>
</FormItem>
</FormField>
@ -120,7 +116,7 @@
<FormDescription>
{{ m.weird_fun_jurgen_arise() }}
</FormDescription>
<FormMessage />
<FormMessage/>
</FormItem>
</FormField>
@ -130,7 +126,10 @@
name="global"
as-child
>
<FormSwitch :title="m.pink_sharp_carp_work()" :description="m.dark_pretty_hyena_link()">
<FormSwitch
:title="m.pink_sharp_carp_work()"
:description="m.dark_pretty_hyena_link()"
>
<Switch
:model-value="value"
@update:model-value="handleChange"

View file

@ -1,5 +1,5 @@
<template>
<Dialog />
<Dialog/>
</template>
<script lang="ts" setup>

View file

@ -1,10 +1,8 @@
<template>
<section class="gap-4 flex flex-col">
<h2 class="text-xl font-bold tracking-tight">
{{ title }}
</h2>
<h2 class="text-xl font-bold tracking-tight">{{ title }}</h2>
<slot />
<slot/>
</section>
</template>

View file

@ -1,10 +1,20 @@
<template>
<form class="grid gap-6" @submit="save">
<Transition name="slide-up">
<Alert v-if="dirty" class="absolute bottom-2 z-10 inset-x-2 w-[calc(100%-1rem)] grid-cols-[calc(var(--spacing)*4)_1fr_auto]!">
<SaveOff class="size-4" />
<Alert
v-if="dirty"
class="absolute bottom-2 z-10 inset-x-2 w-[calc(100%-1rem)] grid-cols-[calc(var(--spacing)*4)_1fr_auto]!"
>
<SaveOff class="size-4"/>
<AlertTitle>Unsaved changes</AlertTitle>
<Button variant="secondary" class="w-full row-span-2" type="button" :disabled="submitting">Apply</Button>
<Button
variant="secondary"
class="w-full row-span-2"
type="button"
:disabled="submitting"
>
Apply
</Button>
<AlertDescription>
Click "apply" to save your changes.
</AlertDescription>
@ -12,55 +22,101 @@
</Transition>
<FormField v-slot="{ handleChange, handleBlur }" name="banner">
<TextInput :title="m.bright_late_osprey_renew()" :description="m.great_level_lamb_sway()">
<Input type="file" accept="image/*" @change="handleChange" @blur="handleBlur" />
<TextInput
:title="m.bright_late_osprey_renew()"
:description="m.great_level_lamb_sway()"
>
<Input
type="file"
accept="image/*"
@change="handleChange"
@blur="handleBlur"
/>
</TextInput>
</FormField>
<FormField v-slot="{ setValue }" name="avatar">
<TextInput :title="m.safe_icy_bulldog_quell()">
<ImageUploader v-model:image="authStore.account!.avatar" @submit-file="(file) => setValue(file)"
@submit-url="(url) => setValue(url)" />
<ImageUploader
v-model:image="authStore.account!.avatar"
@submit-file="(file) => setValue(file)"
@submit-url="(url) => setValue(url)"
/>
</TextInput>
</FormField>
<FormField v-slot="{ componentField }" name="name">
<TextInput :title="m.mild_known_mallard_jolt()" :description="m.lime_dry_skunk_loop()">
<Input v-bind="componentField" />
<TextInput
:title="m.mild_known_mallard_jolt()"
:description="m.lime_dry_skunk_loop()"
>
<Input v-bind="componentField"/>
</TextInput>
</FormField>
<FormField v-slot="{ componentField }" name="username">
<TextInput :title="m.neat_silly_dog_prosper()" :description="m.petty_plane_tadpole_earn()">
<Input v-bind="componentField" />
<TextInput
:title="m.neat_silly_dog_prosper()"
:description="m.petty_plane_tadpole_earn()"
>
<Input v-bind="componentField"/>
</TextInput>
</FormField>
<FormField v-slot="{ componentField }" name="bio">
<TextInput :title="m.next_caring_ladybug_hack()" :description="m.stale_just_anaconda_earn()">
<Textarea rows="10" v-bind="componentField" />
<TextInput
:title="m.next_caring_ladybug_hack()"
:description="m.stale_just_anaconda_earn()"
>
<Textarea rows="10" v-bind="componentField"/>
</TextInput>
</FormField>
<FormField v-slot="{ value, handleChange }" name="fields">
<Fields :title="m.aqua_mealy_toucan_pride()" :value="value" @update:value="handleChange" />
<Fields
:title="m.aqua_mealy_toucan_pride()"
:value="value"
@update:value="handleChange"
/>
</FormField>
<FormField v-slot="{ value, handleChange }" name="bot" as-child>
<SwitchInput :title="m.gaudy_each_opossum_play()" :description="m.grassy_acidic_gadfly_cure()">
<Switch :model-value="value" @update:model-value="handleChange" />
<SwitchInput
:title="m.gaudy_each_opossum_play()"
:description="m.grassy_acidic_gadfly_cure()"
>
<Switch
:model-value="value"
@update:model-value="handleChange"
/>
</SwitchInput>
</FormField>
<FormField v-slot="{ value, handleChange }" name="locked" as-child>
<SwitchInput :title="m.dirty_moving_shark_emerge()" :description="m.bright_fun_mouse_boil()">
<Switch :model-value="value" @update:model-value="handleChange" />
<SwitchInput
:title="m.dirty_moving_shark_emerge()"
:description="m.bright_fun_mouse_boil()"
>
<Switch
:model-value="value"
@update:model-value="handleChange"
/>
</SwitchInput>
</FormField>
<FormField v-slot="{ value, handleChange }" name="discoverable" as-child>
<SwitchInput :title="m.red_vivid_cuckoo_spark()" :description="m.plain_zany_donkey_dart()">
<Switch :model-value="value" @update:model-value="handleChange" />
<FormField
v-slot="{ value, handleChange }"
name="discoverable"
as-child
>
<SwitchInput
:title="m.red_vivid_cuckoo_spark()"
:description="m.plain_zany_donkey_dart()"
>
<Switch
:model-value="value"
@update:model-value="handleChange"
/>
</SwitchInput>
</FormField>
</form>

View file

@ -2,30 +2,64 @@
<FormItem>
<FormLabel>
{{ title }}
<Button type="button" variant="secondary" size="icon" class="ml-auto" @click="addField()" :title="m.front_north_eel_gulp()">
<Plus />
<Button
type="button"
variant="secondary"
size="icon"
class="ml-auto"
@click="addField()"
:title="m.front_north_eel_gulp()"
>
<Plus/>
</Button>
</FormLabel>
<FormControl>
<VueDraggable class="grid gap-4" v-model="list" :animation="200" handle=".drag-handle">
<div v-for="(field, index) in list" :key="field.id"
class="grid items-center grid-cols-[auto_repeat(3,minmax(0,1fr))_auto] gap-2">
<Button as="span" variant="ghost" size="icon" class="drag-handle cursor-grab">
<GripVertical />
<VueDraggable
class="grid gap-4"
v-model="list"
:animation="200"
handle=".drag-handle"
>
<div
v-for="(field, index) in list"
:key="field.id"
class="grid items-center grid-cols-[auto_repeat(3,minmax(0,1fr))_auto] gap-2"
>
<Button
as="span"
variant="ghost"
size="icon"
class="drag-handle cursor-grab"
>
<GripVertical/>
</Button>
<Input :model-value="field.name" placeholder="Name" @update:model-value="
<Input
:model-value="field.name"
placeholder="Name"
@update:model-value="
(e) => updateKey(index, String(e))
" />
<Input :model-value="field.value" placeholder="Value" class="col-span-2" @update:model-value="
"
/>
<Input
:model-value="field.value"
placeholder="Value"
class="col-span-2"
@update:model-value="
(e) => updateValue(index, String(e))
" />
<Button type="button" variant="secondary" size="icon" @click="removeField(index)">
<Trash />
"
/>
<Button
type="button"
variant="secondary"
size="icon"
@click="removeField(index)"
>
<Trash/>
</Button>
</div>
</VueDraggable>
<FormMessage />
<FormMessage/>
</FormControl>
</FormItem>
</template>

View file

@ -6,18 +6,16 @@
variant="ghost"
class="h-fit w-fit p-0 m-0 relative group border overflow-hidden"
>
<Avatar class="size-32" :src="image" :name="displayName" />
<Avatar class="size-32" :src="image" :name="displayName"/>
<div
class="absolute inset-0 bg-background/80 flex group-hover:opacity-100 opacity-0 duration-200 items-center justify-center"
>
<Upload />
<Upload/>
</div>
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>
{{ m.due_hour_husky_prosper() }}
</DialogTitle>
<DialogTitle>{{ m.due_hour_husky_prosper() }}</DialogTitle>
<DialogDescription class="sr-only">
{{ m.suave_broad_albatross_drop() }}
</DialogDescription>
@ -58,7 +56,7 @@
<FormDescription>
{{ m.lime_late_millipede_urge() }}
</FormDescription>
<FormMessage />
<FormMessage/>
</FormItem>
</FormField>
</TabsContent>
@ -83,12 +81,14 @@
placeholder="peter.griffin@fox.com"
/>
</FormControl>
<FormMessage />
<FormMessage/>
<div v-if="value" class="grid gap-4 !mt-4">
<Label>{{
<Label>
{{
m.witty_honest_wallaby_support()
}}</Label>
<Avatar class="size-32" :src="gravatarUrl" />
}}
</Label>
<Avatar class="size-32" :src="gravatarUrl"/>
</div>
</FormItem>
</FormField>
@ -109,12 +109,14 @@
placeholder="https://mysite.com/avatar.webp"
/>
</FormControl>
<FormMessage />
<FormMessage/>
<div v-if="value" class="grid gap-4 !mt-4">
<Label>{{
<Label>
{{
m.witty_honest_wallaby_support()
}}</Label>
<Avatar class="size-32" :src="value" />
}}
</Label>
<Avatar class="size-32" :src="value"/>
</div>
</FormItem>
</FormField>

View file

@ -1,13 +1,17 @@
<template>
<Card class="grid gap-3 text-sm max-w-sm">
<dl class="grid gap-3">
<div v-for="[key, value] of data" :key="key" class="flex flex-row items-baseline justify-between gap-4 truncate">
<dt class="text-muted-foreground">
{{ key }}
</dt>
<dd class="font-mono" v-if="typeof value === 'string'">{{ value }}</dd>
<div
v-for="[key, value] of data"
:key="key"
class="flex flex-row items-baseline justify-between gap-4 truncate"
>
<dt class="text-muted-foreground">{{ key }}</dt>
<dd class="font-mono" v-if="typeof value === 'string'">
{{ value }}
</dd>
<dd class="font-mono" v-else>
<component :is="value" />
<component :is="value"/>
</dd>
</div>
</dl>

View file

@ -1,14 +1,14 @@
<template>
<Base :pref="pref" :name="name" v-slot="{ setValue, value }">
<Switch @update:model-value="setValue" :model-value="value" />
</Base>
<TypeBase :pref="pref" :name="name" v-slot="{ setValue, value }">
<Switch @update:model-value="setValue" :model-value="value"/>
</TypeBase>
</template>
<script lang="ts" setup>
import { Switch } from "~/components/ui/switch";
import type { preferences as prefs } from "../preferences";
import type { BooleanPreference } from "../types";
import Base from "./base.vue";
import TypeBase from "./type-base.vue";
const { pref, name } = defineProps<{
pref: BooleanPreference;

View file

@ -1,19 +1,21 @@
<template>
<Collapsible as-child>
<Base :name="name" :pref="pref">
<template #default>
<CollapsibleTrigger as-child>
<Button variant="outline">
Open code
</Button>
</CollapsibleTrigger>
</template>
<template #extra="{ setValue, value }">
<CollapsibleContent class="col-span-2 mt-2">
<Textarea :rows="10" :model-value="value" @update:model-value="setValue" />
</CollapsibleContent>
</template>
</Base>
<TypeBase :name="name" :pref="pref">
<template #default>
<CollapsibleTrigger as-child>
<Button variant="outline">Open code</Button>
</CollapsibleTrigger>
</template>
<template #extra="{ setValue, value }">
<CollapsibleContent class="col-span-2 mt-2">
<Textarea
:rows="10"
:model-value="value"
@update:model-value="setValue"
/>
</CollapsibleContent>
</template>
</TypeBase>
</Collapsible>
</template>
@ -27,7 +29,7 @@ import {
import { Textarea } from "~/components/ui/textarea";
import type { preferences as prefs } from "../preferences";
import type { CodePreference } from "../types";
import Base from "./base.vue";
import TypeBase from "./type-base.vue";
const { pref, name } = defineProps<{
pref: CodePreference;

View file

@ -1,25 +1,27 @@
<template>
<Base :pref="pref" :name="name" v-slot="{ setValue, value }">
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="outline">
Pick
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent class="w-56">
<DropdownMenuCheckboxItem v-for="[option, title] in Object.entries(pref.options.options)" :key="option"
:model-value="value.includes(option)" @update:model-value="checked => {
<TypeBase :pref="pref" :name="name" v-slot="{ setValue, value }">
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="outline">Pick</Button>
</DropdownMenuTrigger>
<DropdownMenuContent class="w-56">
<DropdownMenuCheckboxItem
v-for="[option, title] in Object.entries(pref.options.options)"
:key="option"
:model-value="value.includes(option)"
@update:model-value="checked => {
if (checked) {
setValue([...value, option]);
} else {
setValue(value.filter((v: any) => v !== option));
}
}">
{{ title }}
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
</Base>
}"
>
{{ title }}
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
</TypeBase>
</template>
<script lang="ts" setup>
@ -32,7 +34,7 @@ import {
} from "~/components/ui/dropdown-menu";
import type { preferences as prefs } from "../preferences";
import type { MultiSelectPreference } from "../types";
import Base from "./base.vue";
import TypeBase from "./type-base.vue";
const { pref, name } = defineProps<{
pref: MultiSelectPreference<string>;

View file

@ -1,13 +1,19 @@
<template>
<Base :pref="pref" :name="name" v-slot="{ setValue, value }">
<NumberField :model-value="value" @update:model-value="setValue" :min="pref.options.min" :max="pref.options.max" :step="pref.options.integer ? 1 : pref.options.step">
<TypeBase :pref="pref" :name="name" v-slot="{ setValue, value }">
<NumberField
:model-value="value"
@update:model-value="setValue"
:min="pref.options.min"
:max="pref.options.max"
:step="pref.options.integer ? 1 : pref.options.step"
>
<NumberFieldContent>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
<NumberFieldDecrement/>
<NumberFieldInput/>
<NumberFieldIncrement/>
</NumberFieldContent>
</NumberField>
</Base>
</TypeBase>
</template>
<script lang="ts" setup>
@ -20,7 +26,7 @@ import {
} from "~/components/ui/number-field";
import type { preferences as prefs } from "../preferences";
import type { NumberPreference } from "../types";
import Base from "./base.vue";
import TypeBase from "./type-base.vue";
const { pref, name } = defineProps<{
pref: NumberPreference;

View file

@ -1,18 +1,21 @@
<template>
<Base :pref="pref" :name="name" v-slot="{ setValue, value }">
<TypeBase :pref="pref" :name="name" v-slot="{ setValue, value }">
<Select :model-value="value" @update:model-value="setValue">
<SelectTrigger>
<SelectValue placeholder="Select an option" />
<SelectValue placeholder="Select an option"/>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem v-for="[val, title] in Object.entries(pref.options.options)" :value="val">
<SelectItem
v-for="[val, title] in Object.entries(pref.options.options)"
:value="val"
>
{{ title }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</Base>
</TypeBase>
</template>
<script lang="ts" setup>
@ -26,7 +29,7 @@ import {
} from "~/components/ui/select";
import type { preferences as prefs } from "../preferences";
import type { SelectPreference } from "../types";
import Base from "./base.vue";
import TypeBase from "./type-base.vue";
const { pref, name } = defineProps<{
pref: SelectPreference<string>;

View file

@ -1,14 +1,18 @@
<template>
<Base :pref="pref" :name="name" v-slot="{ setValue, value }">
<Input placeholder="Content here..." :model-value="value" @update:model-value="setValue" />
</Base>
<TypeBase :pref="pref" :name="name" v-slot="{ setValue, value }">
<Input
placeholder="Content here..."
:model-value="value"
@update:model-value="setValue"
/>
</TypeBase>
</template>
<script lang="ts" setup>
import { Input } from "~/components/ui/input";
import type { preferences as prefs } from "../preferences";
import type { TextPreference } from "../types";
import Base from "./base.vue";
import TypeBase from "./type-base.vue";
const { pref, name } = defineProps<{
pref: TextPreference;

View file

@ -1,14 +1,22 @@
<template>
<div class="grid grid-cols-[minmax(0,1fr)_auto] gap-2 hover:bg-muted/40 duration-75 p-4">
<div
class="grid grid-cols-[minmax(0,1fr)_auto] gap-2 hover:bg-muted/40 duration-75 p-4"
>
<div class="flex flex-col gap-1">
<h3 class="text-sm font-semibold tracking-tight">{{ pref.options.name }}</h3>
<small v-if="pref.options.description" class="text-xs font-medium leading-none text-muted-foreground">{{
pref.options.description }}</small>
<h3 class="text-sm font-semibold tracking-tight">
{{ pref.options.name }}
</h3>
<small
v-if="pref.options.description"
class="text-xs font-medium leading-none text-muted-foreground"
>{{
pref.options.description }}</small
>
</div>
<div class="flex items-center justify-end">
<slot :value="value" :set-value="setValue" />
<slot :value="value" :set-value="setValue"/>
</div>
<slot name="extra" :value="value" :set-value="setValue" />
<slot name="extra" :value="value" :set-value="setValue"/>
</div>
</template>

View file

@ -1,19 +1,21 @@
<template>
<Collapsible as-child>
<Base :pref="pref" :name="name">
<template #default>
<CollapsibleTrigger as-child>
<Button variant="outline">
Edit URL
</Button>
</CollapsibleTrigger>
</template>
<template #extra="{ setValue, value }">
<CollapsibleContent class="col-span-2 mt-2">
<UrlInput placeholder="Type URL or domain here..." :model-value="value" @update:model-value="setValue" />
</CollapsibleContent>
</template>
</Base>
<TypeBase :pref="pref" :name="name">
<template #default>
<CollapsibleTrigger as-child>
<Button variant="outline">Edit URL</Button>
</CollapsibleTrigger>
</template>
<template #extra="{ setValue, value }">
<CollapsibleContent class="col-span-2 mt-2">
<UrlInput
placeholder="Type URL or domain here..."
:model-value="value"
@update:model-value="setValue"
/>
</CollapsibleContent>
</template>
</TypeBase>
</Collapsible>
</template>
@ -27,7 +29,7 @@ import {
import { Input, UrlInput } from "~/components/ui/input";
import type { preferences as prefs } from "../preferences";
import type { TextPreference } from "../types";
import Base from "./base.vue";
import TypeBase from "./type-base.vue";
const { pref, name } = defineProps<{
pref: TextPreference;

View file

@ -1,9 +1,7 @@
<template>
<Avatar :class="['rounded-md bg-secondary']">
<AvatarFallback v-if="name">
{{ getInitials(name) }}
</AvatarFallback>
<AvatarImage v-if="src" :src="src" :alt="`${name}'s avatar`" />
<AvatarFallback v-if="name">{{ getInitials(name) }}</AvatarFallback>
<AvatarImage v-if="src" :src="src" :alt="`${name}'s avatar`"/>
</Avatar>
</template>

View file

@ -1,60 +1,72 @@
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<slot />
<slot/>
</DropdownMenuTrigger>
<DropdownMenuContent class="min-w-56">
<DropdownMenuGroup>
<DropdownMenuItem as="button" @click="copyText(account.username)">
<AtSign />
<DropdownMenuItem
as="button"
@click="copyText(account.username)"
>
<AtSign/>
{{ m.cool_dark_tapir_belong() }}
</DropdownMenuItem>
<DropdownMenuItem as="button" @click="copyText(JSON.stringify(account, null, 4))">
<Code />
<DropdownMenuItem
as="button"
@click="copyText(JSON.stringify(account, null, 4))"
>
<Code/>
{{ m.yummy_moving_scallop_sail() }}
</DropdownMenuItem>
<DropdownMenuItem as="button" @click="copyText(account.id)">
<Hash />
<Hash/>
{{ m.sunny_zany_jellyfish_pop() }}
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuSeparator/>
<DropdownMenuGroup>
<DropdownMenuItem as="button" @click="copyText(url)">
<Link />
<Link/>
{{ m.ago_new_pelican_drip() }}
</DropdownMenuItem>
<DropdownMenuItem as="button" @click="copyText(account.url)">
<Link />
<Link/>
{{ m.solid_witty_zebra_walk() }}
</DropdownMenuItem>
<DropdownMenuItem as="a" v-if="isRemote" target="_blank" rel="noopener noreferrer" :href="account.url">
<ExternalLink />
<DropdownMenuItem
as="a"
v-if="isRemote"
target="_blank"
rel="noopener noreferrer"
:href="account.url"
>
<ExternalLink/>
{{ m.active_trite_lark_inspire() }}
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator v-if="authStore.isSignedIn && !isMe" />
<DropdownMenuSeparator v-if="authStore.isSignedIn && !isMe"/>
<DropdownMenuGroup v-if="authStore.isSignedIn && !isMe">
<DropdownMenuItem as="button" @click="muteUser(account.id)">
<VolumeX />
<VolumeX/>
{{ m.spare_wild_mole_intend() }}
</DropdownMenuItem>
<DropdownMenuItem as="button" @click="blockUser(account.id)">
<Ban />
<Ban/>
{{ m.misty_soft_sparrow_vent() }}
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator v-if="isRemote" />
<DropdownMenuSeparator v-if="isRemote"/>
<DropdownMenuGroup v-if="isRemote">
<DropdownMenuItem as="button" @click="refresh">
<RefreshCw />
<RefreshCw/>
{{ m.slow_chunky_chipmunk_hush() }}
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator v-if="authStore.isSignedIn && !isMe" />
<DropdownMenuSeparator v-if="authStore.isSignedIn && !isMe"/>
<DropdownMenuGroup v-if="authStore.isSignedIn && !isMe">
<DropdownMenuItem as="button" :disabled="true">
<Flag />
<Flag/>
{{ m.great_few_jaguar_rise() }}
</DropdownMenuItem>
</DropdownMenuGroup>

View file

@ -2,8 +2,8 @@
<Tooltip>
<TooltipTrigger :as-child="true">
<Badge variant="default" class="gap-1">
<BadgeCheck v-if="verified" />
<img v-else-if="icon" :src="icon" alt="" class="size-4 rounded" />
<BadgeCheck v-if="verified"/>
<img v-else-if="icon" :src="icon" alt="" class="size-4 rounded">
{{ name }}
</Badge>
</TooltipTrigger>

View file

@ -1,5 +1,7 @@
<template>
<Row class="gap-2" wrap
<Row
class="gap-2"
wrap
v-if="isDeveloper || account.bot || roles.length > 0"
>
<ProfileBadge

View file

@ -1,10 +1,16 @@
<template>
<Col class="gap-y-4">
<Col v-for="field in fields" :key="field.name" class="gap-1 break-words">
<HeadingSmall v-render-emojis="emojis">{{ field.name }}</HeadingSmall>
<Html v-html="field.value" v-render-emojis="emojis" />
</Col>
</Col>
<Column class="gap-y-4">
<Column
v-for="field in fields"
:key="field.name"
class="gap-1 break-words"
>
<HeadingSmall v-render-emojis="emojis">
{{ field.name }}
</HeadingSmall>
<Html v-html="field.value" v-render-emojis="emojis"/>
</Column>
</Column>
</template>
<script lang="ts" setup>
@ -12,7 +18,7 @@ import type { CustomEmoji, Field } from "@versia/client/schemas";
import type { z } from "zod";
import HeadingSmall from "~/components/typography/headings/small.vue";
import Html from "../typography/html.vue";
import Col from "../typography/layout/col.vue";
import Column from "../typography/layout/col.vue";
defineProps<{
fields: z.infer<typeof Field>[];

View file

@ -6,7 +6,7 @@
:src="header"
alt=""
class="object-cover w-full h-full"
/>
>
<!-- Shadow overlay at the bottom -->
<div
class="absolute bottom-0 w-full h-1/3 bg-gradient-to-b from-black/0 to-black/40"
@ -15,11 +15,7 @@
<div
class="absolute bottom-0 translate-y-1/3 left-4 flex flex-row items-start gap-2"
>
<Avatar
class="size-32 border"
:src="avatar"
:name="displayName"
/>
<Avatar class="size-32 border" :src="avatar" :name="displayName"/>
</div>
</CardHeader>
</template>

View file

@ -1,7 +1,11 @@
<template>
<Button variant="secondary" :disabled="isLoading || relationship?.requested" v-if="!isMe && authStore.isSignedIn"
@click="relationship?.following ? unfollow() : follow()">
<Loader v-if="isLoading" class="animate-spin" />
<Button
variant="secondary"
:disabled="isLoading || relationship?.requested"
v-if="!isMe && authStore.isSignedIn"
@click="relationship?.following ? unfollow() : follow()"
>
<Loader v-if="isLoading" class="animate-spin"/>
<span v-else>
{{
relationship?.following

View file

@ -1,24 +1,24 @@
<template>
<Row class="gap-2 w-full justify-around">
<Col centered>
<Column centered>
<Bold>{{ noteCount }}</Bold>
<Small muted>{{ m.real_gray_stork_seek() }}</Small>
</Col>
<Col centered>
</Column>
<Column centered>
<Bold>{{ followerCount }}</Bold>
<Small muted>{{ m.teal_helpful_parakeet_hike() }}</Small>
</Col>
<Col centered>
</Column>
<Column centered>
<Bold>{{ followingCount }}</Bold>
<Small muted>{{ m.aloof_royal_samuel_startle() }}</Small>
</Col>
</Column>
</Row>
</template>
<script lang="ts" setup>
import * as m from "~~/paraglide/messages.js";
import Bold from "../typography/bold.vue";
import Col from "../typography/layout/col.vue";
import Column from "../typography/layout/col.vue";
import Row from "../typography/layout/row.vue";
import Small from "../typography/small.vue";

View file

@ -1,33 +1,45 @@
<template>
<Card class="gap-4">
<ProfileHeader :header="account.header" :avatar="account.avatar" :display-name="account.display_name" />
<ProfileHeader
:header="account.header"
:avatar="account.avatar"
:display-name="account.display_name"
/>
<Row class="justify-end gap-2">
<ProfileRelationshipActions :account="account" />
<ProfileRelationshipActions :account="account"/>
<ProfileActions :account="account">
<Button variant="secondary" size="icon">
<Ellipsis />
<Ellipsis/>
</Button>
</ProfileActions>
</Row>
<Col class="justify-center">
<Column class="justify-center">
<Text class="font-bold" v-render-emojis="account.emojis">
{{ account.display_name }}
</Text>
<Address :username="username" :domain="domain" />
</Col>
<ProfileBadges :account="account" />
<Html v-html="account.note" v-render-emojis="account.emojis" />
<Separator />
<ProfileFields v-if="account.fields.length > 0" :fields="account.fields" :emojis="account.emojis" />
<Separator v-if="account.fields.length > 0" />
<Address :username="username" :domain="domain"/>
</Column>
<ProfileBadges :account="account"/>
<Html v-html="account.note" v-render-emojis="account.emojis"/>
<Separator/>
<ProfileFields
v-if="account.fields.length > 0"
:fields="account.fields"
:emojis="account.emojis"
/>
<Separator v-if="account.fields.length > 0"/>
<Row>
<HeadingSmall class="flex items-center gap-1">
<CalendarDays class="size-4" /> {{ formattedCreationDate }}
<CalendarDays class="size-4"/>
{{ formattedCreationDate }}
</HeadingSmall>
</Row>
<Separator />
<ProfileStats :follower-count="account.followers_count" :following-count="account.following_count"
:note-count="account.statuses_count" />
<Separator/>
<ProfileStats
:follower-count="account.followers_count"
:following-count="account.following_count"
:note-count="account.statuses_count"
/>
</Card>
</template>
@ -41,7 +53,7 @@ import { Separator } from "~/components/ui/separator";
import { getLocale } from "~~/paraglide/runtime";
import HeadingSmall from "../typography/headings/small.vue";
import Html from "../typography/html.vue";
import Col from "../typography/layout/col.vue";
import Column from "../typography/layout/col.vue";
import Row from "../typography/layout/row.vue";
import Text from "../typography/text.vue";
import Address from "./address.vue";

View file

@ -5,7 +5,7 @@
:src="account.header"
alt=""
class="object-cover w-full h-full"
/>
>
<!-- Shadow overlay at the bottom -->
<div
class="absolute bottom-0 w-full h-1/3 bg-gradient-to-b from-black/0 to-black/40"
@ -26,14 +26,14 @@
<Text class="font-bold" v-render-emojis="account.emojis">
{{ account.display_name }}
</Text>
<Address :username="username" :domain="domain" />
<Address :username="username" :domain="domain"/>
</div>
<Html
v-html="account.note"
v-render-emojis="account.emojis"
class="mt-4 max-h-72 overflow-y-auto"
/>
<Separator v-if="account.fields.length > 0" class="mt-4" />
<Separator v-if="account.fields.length > 0" class="mt-4"/>
<ProfileFields
v-if="account.fields.length > 0"
:fields="account.fields"

View file

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

View file

@ -1,7 +1,7 @@
<template>
<Dialog>
<DialogTrigger as-child>
<slot />
<slot/>
</DialogTrigger>
<DialogContent>
<DialogHeader>
@ -11,16 +11,31 @@
</DialogDescription>
</DialogHeader>
<div v-if="authStore.identities.length > 0" class="grid gap-4 py-2">
<div v-for="identity of authStore.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="authStore.identity?.id !== identity.id"
@click="authStore.setActiveIdentity(identity.id)" variant="outline">
<div
v-for="identity of authStore.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="authStore.identity?.id !== identity.id"
@click="authStore.setActiveIdentity(identity.id)"
variant="outline"
>
Switch
</Button>
<Button @click="signOutAction(identity.id)" variant="outline" size="icon"
:title="m.sharp_big_mallard_reap()">
<LogOut />
<Button
@click="signOutAction(identity.id)"
variant="outline"
size="icon"
:title="m.sharp_big_mallard_reap()"
>
<LogOut/>
</Button>
</div>
</div>
@ -31,11 +46,11 @@
</div>
<DialogFooter>
<Button :as="NuxtLink" href="/register" variant="outline">
<UserPlus />
<UserPlus/>
{{ m.honest_few_baboon_pop() }}
</Button>
<Button @click="signInAction">
<LogIn />
<LogIn/>
{{ m.sunny_pink_hyena_walk() }}
</Button>
</DialogFooter>

View file

@ -26,32 +26,54 @@ const authStore = useAuthStore();
<SidebarMenu class="gap-3">
<SidebarMenuItem>
<AccountManager>
<SidebarMenuButton v-if="authStore.account && authStore.instance" size="lg">
<TinyCard :account="authStore.account" :domain="authStore.instance.domain" naked />
<ChevronsUpDown class="ml-auto size-4" />
<SidebarMenuButton
v-if="authStore.account && authStore.instance"
size="lg"
>
<TinyCard
:account="authStore.account"
:domain="authStore.instance.domain"
naked
/>
<ChevronsUpDown class="ml-auto size-4"/>
</SidebarMenuButton>
<SidebarMenuButton v-else>
<UserPlus />
<UserPlus/>
{{ m.sunny_pink_hyena_walk() }}
<ChevronsUpDown class="ml-auto size-4" />
<ChevronsUpDown class="ml-auto size-4"/>
</SidebarMenuButton>
</AccountManager>
</SidebarMenuItem>
<SidebarMenuItem class="flex flex-col gap-2">
<Button v-if="authStore.isSignedIn" variant="default" size="lg" class="w-full group-data-[collapsible=icon]:px-4"
@click="useEvent('composer:open')">
<Pen />
<Button
v-if="authStore.isSignedIn"
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="authStore.isSignedIn" size="lg" variant="secondary" @click="useEvent('preferences:open')">
<Cog />
<Button
v-if="authStore.isSignedIn"
size="lg"
variant="secondary"
@click="useEvent('preferences:open')"
>
<Cog/>
Preferences
</Button>
<Button v-if="$pwa?.needRefresh" variant="destructive" size="lg"
class="w-full group-data-[collapsible=icon]:px-4" @click="$pwa?.updateServiceWorker(true)">
<DownloadCloud />
<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() }}
</span>

View file

@ -14,7 +14,10 @@ const authStore = useAuthStore();
<SidebarMenu>
<SidebarMenuItem>
<NuxtLink href="/">
<InstanceSmallCard v-if="authStore.instance" :instance="authStore.instance" />
<InstanceSmallCard
v-if="authStore.instance"
:instance="authStore.instance"
/>
</NuxtLink>
</SidebarMenuItem>
</SidebarMenu>

View file

@ -1,11 +1,13 @@
<template>
<Sidebar collapsible="offcanvas">
<InstanceHeader />
<InstanceHeader/>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>{{
<SidebarGroupLabel>
{{
m.trite_real_sawfish_drum()
}}</SidebarGroupLabel>
}}
</SidebarGroupLabel>
<NavItems
:items="
sidebarConfig.other.filter((i) =>
@ -15,8 +17,8 @@
/>
</SidebarGroup>
</SidebarContent>
<FooterActions />
<SidebarRail />
<FooterActions/>
<SidebarRail/>
</Sidebar>
</template>

View file

@ -32,7 +32,7 @@ defineProps<{
<SidebarMenuItem>
<CollapsibleTrigger as-child>
<SidebarMenuButton :tooltip="item.title">
<component :is="item.icon" />
<component :is="item.icon"/>
{{ item.title }}
<ChevronRight
class="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180"

View file

@ -16,7 +16,7 @@ defineProps<{
<SidebarMenuItem v-for="item in items" :key="item.title">
<SidebarMenuButton as-child>
<NuxtLink :href="item.url">
<component :is="item.icon" />
<component :is="item.icon"/>
<span>{{ item.title }}</span>
</NuxtLink>
</SidebarMenuButton>

View file

@ -6,7 +6,7 @@
style="--sidebar-width: 24rem; --sidebar-width-mobile: 18rem"
>
<SidebarContent class="overflow-y-auto *:p-2 *:gap-2">
<NotificationsTimeline />
<NotificationsTimeline/>
</SidebarContent>
</Sidebar>
</template>

View file

@ -14,15 +14,18 @@ const authStore = useAuthStore();
</script>
<template>
<LeftSidebar />
<LeftSidebar/>
<main class="grow h-dvh overflow-y-auto">
<header
v-if="showTimelines"
class="flex h-16 items-center bg-background/80 backdrop-blur-2xl sticky top-0 inset-x-0 z-10 p-4"
>
<Timelines />
<Timelines/>
</header>
<slot />
<slot/>
</main>
<RightSidebar v-if="authStore.isSignedIn" v-show="preferences.display_notifications_sidebar" />
<RightSidebar
v-if="authStore.isSignedIn"
v-show="preferences.display_notifications_sidebar"
/>
</template>

View file

@ -1,7 +1,15 @@
<template>
<Timeline type="status" :items="items" :is-loading="isLoading" :has-reached-end="hasReachedEnd"
:error="error" :load-next="loadNext" :load-prev="loadPrev" :remove-item="removeItem"
:update-item="updateItem" />
<Timeline
type="status"
:items="items"
:is-loading="isLoading"
:has-reached-end="hasReachedEnd"
:error="error"
:load-next="loadNext"
:load-prev="loadPrev"
:remove-item="removeItem"
:update-item="updateItem"
/>
</template>
<script lang="ts" setup>

View file

@ -1,7 +1,15 @@
<template>
<Timeline type="status" :items="items" :is-loading="isLoading" :has-reached-end="hasReachedEnd"
:error="error" :load-next="loadNext" :load-prev="loadPrev" :remove-item="removeItem"
:update-item="updateItem" />
<Timeline
type="status"
:items="items"
:is-loading="isLoading"
:has-reached-end="hasReachedEnd"
:error="error"
:load-next="loadNext"
:load-prev="loadPrev"
:remove-item="removeItem"
:update-item="updateItem"
/>
</template>
<script lang="ts" setup>

View file

@ -1,7 +1,15 @@
<template>
<Timeline type="status" :items="items" :is-loading="isLoading" :has-reached-end="hasReachedEnd"
:error="error" :load-next="loadNext" :load-prev="loadPrev" :remove-item="removeItem"
:update-item="updateItem" />
<Timeline
type="status"
:items="items"
:is-loading="isLoading"
:has-reached-end="hasReachedEnd"
:error="error"
:load-next="loadNext"
:load-prev="loadPrev"
:remove-item="removeItem"
:update-item="updateItem"
/>
</template>
<script lang="ts" setup>

View file

@ -1,7 +1,15 @@
<template>
<Timeline type="status" :items="items" :is-loading="isLoading" :has-reached-end="hasReachedEnd"
:error="error" :load-next="loadNext" :load-prev="loadPrev" :remove-item="removeItem"
:update-item="updateItem" />
<Timeline
type="status"
:items="items"
:is-loading="isLoading"
:has-reached-end="hasReachedEnd"
:error="error"
:load-next="loadNext"
:load-prev="loadPrev"
:remove-item="removeItem"
:update-item="updateItem"
/>
</template>
<script lang="ts" setup>

View file

@ -1,7 +1,15 @@
<template>
<Timeline type="notification" :items="items" :is-loading="isLoading"
:has-reached-end="hasReachedEnd" :error="error" :load-next="loadNext" :load-prev="loadPrev"
:remove-item="removeItem" :update-item="updateItem" />
<Timeline
type="notification"
:items="items"
:is-loading="isLoading"
:has-reached-end="hasReachedEnd"
:error="error"
:load-next="loadNext"
:load-prev="loadPrev"
:remove-item="removeItem"
:update-item="updateItem"
/>
</template>
<script lang="ts" setup>

View file

@ -1,7 +1,15 @@
<template>
<Timeline type="status" :items="items" :is-loading="isLoading" :has-reached-end="hasReachedEnd"
:error="error" :load-next="loadNext" :load-prev="loadPrev" :remove-item="removeItem"
:update-item="updateItem" />
<Timeline
type="status"
:items="items"
:is-loading="isLoading"
:has-reached-end="hasReachedEnd"
:error="error"
:load-next="loadNext"
:load-prev="loadPrev"
:remove-item="removeItem"
:update-item="updateItem"
/>
</template>
<script lang="ts" setup>

View file

@ -1,6 +1,11 @@
<template>
<component :is="itemComponent" :note="type === 'status' ? item : undefined" :notification="type === 'notification' ? item : (undefined as any)" @update="$emit('update', $event)"
@delete="$emit('delete', item?.id)" />
<component
:is="itemComponent"
:note="type === 'status' ? item : undefined"
:notification="type === 'notification' ? item : (undefined as any)"
@update="$emit('update', $event)"
@delete="$emit('delete', item?.id)"
/>
</template>
<script lang="ts" setup>

View file

@ -1,5 +1,5 @@
<template>
<slot />
<slot/>
</template>
<script lang="ts" setup>
@ -23,4 +23,4 @@ onBeforeRouteLeave(() => {
yStored.value[route.fullPath] = y.value;
yStored.value = { ...yStored.value };
});
</script>
</script>

View file

@ -2,27 +2,25 @@
<div
role="status"
class="flex flex-col gap-4 items-center *:max-w-2xl *:w-full p-4"
>
>
<TimelineItem
:type="type"
v-for="item in items"
:key="item.id"
:item="item"
@update="updateItem"
@delete="removeItem"
:type="type"
v-for="item in items"
:key="item.id"
:item="item"
@update="updateItem"
@delete="removeItem"
/>
<Spinner v-if="isLoading" />
<Spinner v-if="isLoading"/>
<div v-if="error" class="timeline-error">
{{ error.message }}
</div>
<div v-if="error" class="timeline-error">{{ error.message }}</div>
<!-- If there are some posts, but the user scrolled to the end -->
<ReachedEnd v-if="hasReachedEnd && items.length > 0" />
<ReachedEnd v-if="hasReachedEnd && items.length > 0"/>
<!-- If there are no posts at all -->
<NoPosts v-else-if="hasReachedEnd && items.length === 0" />
<NoPosts v-else-if="hasReachedEnd && items.length === 0"/>
<div v-else-if="!preferences.infinite_scroll" class="py-10 px-4">
<Button

Some files were not shown because too many files have changed in this diff Show more