Compare commits

...

4 commits

Author SHA1 Message Date
Jesse Wierzbinski 53b71afdd5
refactor: ♻️ Refactor typography code in notes and profiles
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 1s
Deploy to GitHub Pages / build (push) Failing after 1s
Deploy to GitHub Pages / deploy (push) Has been skipped
Docker / build (push) Failing after 1s
Mirror to Codeberg / Mirror (push) Failing after 0s
2025-07-10 05:13:42 +02:00
Jesse Wierzbinski 97733c18ee
refactor: 💄 Redesign sidebar instance header 2025-07-10 03:58:45 +02:00
Jesse Wierzbinski 5ef26f03a4
style: 💄 Update colors 2025-07-10 03:28:46 +02:00
Jesse Wierzbinski e1e4709d19
chore: ⬆️ Upgrade dependencies 2025-07-10 03:14:49 +02:00
31 changed files with 2047 additions and 1526 deletions

View file

@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.0.5/schema.json",
"$schema": "https://biomejs.dev/schemas/2.1.1/schema.json",
"assist": { "actions": { "source": { "organizeImports": "on" } } },
"linter": {
"enabled": true,

626
bun.lock

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,41 @@
<template>
<Card class="grid grid-cols-[auto_1fr] gap-2">
<Avatar :src="instance.thumbnail?.url ??
'https://cdn.versia.pub/branding/icon.svg'
" :name="instance.title" />
<div class="grid text-sm leading-tight *:line-clamp-1">
<span class="truncate font-semibold">
{{
instance.domain
}}
</span>
<span class="line-clamp-3 text-xs">
{{
instance.versia_version || instance.version
}}
</span>
</div>
<h1 class="line-clamp-1 text-sm font-semibold col-span-2">
{{
instance.title
}}
</h1>
<p class="line-clamp-5 text-xs col-span-2">
{{
instance.description
}}
</p>
</Card>
</template>
<script lang="ts" setup>
import type { Instance } from "@versia/client/schemas";
import type z from "zod";
import Avatar from "../profiles/avatar.vue";
import { Card } from "../ui/card";
const { instance } = defineProps<{
instance: z.infer<typeof Instance>;
}>();
</script>

View file

@ -1,33 +0,0 @@
<template>
<span :class="cn('text-primary group', $props.class)">
<span class="group-hover:hidden">
<slot />
</span>
<span class="hidden group-hover:inline">
<span @click="copyText"
class="select-none cursor-pointer space-x-1">
<Clipboard class="size-4 -translate-y-0.5 inline" />
{{ m.clean_yummy_owl_reside() }}
</span>
</span>
</span>
</template>
<script lang="tsx" setup>
import { Check, Clipboard } from "lucide-vue-next";
import type { HTMLAttributes } from "vue";
import { toast } from "vue-sonner";
import { cn } from "@/lib/utils";
import * as m from "~/paraglide/messages.js";
const { text } = defineProps<{
text: string;
class?: HTMLAttributes["class"];
}>();
const { copy } = useClipboard();
const copyText = () => {
copy(text);
toast.success("Copied to clipboard");
};
</script>

View file

@ -1,5 +1,5 @@
<template>
<div class="rounded flex flex-row items-center gap-3">
<div class="rounded grid grid-cols-[auto_1fr_auto] items-center gap-3">
<HoverCard v-model:open="popupOpen" @update:open="() => {
if (!preferences.popup_avatar_hover) {
popupOpen = false;
@ -16,24 +16,18 @@
<SmallCard :account="author" />
</HoverCardContent>
</HoverCard>
<div
:class="cn('flex flex-col gap-0.5 justify-center flex-1 text-left leading-tight', smallLayout && 'text-sm')">
<span class="truncate font-semibold" v-render-emojis="author.emojis">{{
<Col
:class="smallLayout && 'text-sm'">
<Text class="font-semibold" v-render-emojis="author.emojis">{{
author.display_name
}}</span>
<span class="truncate text-sm tracking-tight">
<span>
<span
class="font-semibold bg-gradient-to-tr from-pink-700 dark:from-indigo-400 via-purple-700 dark:via-purple-400 to-indigo-700 dark:to-indigo-400 text-transparent bg-clip-text">
@{{ username }}
</span>
<span class="text-muted-foreground">{{ instance && "@" }}{{ instance }}</span>
</span>
}}</Text>
<div class="-mt-1">
<Address as="span" :username="username" :domain="instance" />
&middot;
<span class="text-muted-foreground ml-auto tracking-normal" :title="fullTime">{{ timeAgo }}</span>
</span>
</div>
<div class="flex flex-col gap-1 h-full justify-center items-end" v-if="!smallLayout">
<Text as="span" muted class="ml-auto tracking-normal" :title="fullTime">{{ timeAgo }}</Text>
</div>
</Col>
<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" />
@ -52,8 +46,12 @@ import { AtSign, Globe, Lock, LockOpen } from "lucide-vue-next";
import type { z } from "zod";
import { cn } from "@/lib/utils";
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 Row from "../typography/layout/row.vue";
import Text from "../typography/text.vue";
import {
HoverCard,
HoverCardContent,

View file

@ -8,13 +8,7 @@
follower.display_name
}}</span>
<span class="truncate tracking-tight">
<CopyableText :text="follower.acct">
<span
class="font-semibold bg-gradient-to-tr from-pink-700 dark:from-indigo-400 via-purple-700 dark:via-purple-400 to-indigo-700 dark:to-indigo-400 text-transparent bg-clip-text">
@{{ username }}
</span>
<span class="text-muted-foreground">{{ instance && "@" }}{{ instance }}</span>
</CopyableText>
<Address :username="username" :domain="domain" />
</span>
</div>
</div>
@ -39,9 +33,9 @@ import type { Account } from "@versia/client/schemas";
import { Check, Loader, X } from "lucide-vue-next";
import { toast } from "vue-sonner";
import type { z } from "zod";
import CopyableText from "~/components/notes/copyable-text.vue";
import { Button } from "~/components/ui/button";
import * as m from "~/paraglide/messages.js";
import Address from "../profiles/address.vue";
import Avatar from "../profiles/avatar.vue";
const { follower } = defineProps<{
@ -50,7 +44,7 @@ const { follower } = defineProps<{
const loading = ref(true);
const followerUrl = `/@${follower.acct}`;
const [username, instance] = follower.acct.split("@");
const [username, domain] = follower.acct.split("@");
const { relationship } = useRelationship(client, follower.id);
// TODO: Add "followed" notification

View file

@ -0,0 +1,15 @@
<template>
<Text class="font-semibold text-sm tracking-tight">
<span class="text-accent-foreground">@{{ username }}</span>
<span v-if="domain" class="text-muted-foreground">@{{ domain }}</span>
</Text>
</template>
<script lang="ts" setup>
import Text from "../typography/text.vue";
const { username, domain } = defineProps<{
username: string;
domain?: string;
}>();
</script>

View file

@ -1,31 +1,27 @@
<template>
<Tooltip>
<TooltipTrigger :as-child="true">
<Badge variant="outline" class="gap-1">
<svg viewBox="0 0 22 22" v-if="verified" aria-hidden="true" class="size-4 fill-secondary-foreground">
<g>
<path
d="M20.396 11c-.018-.646-.215-1.275-.57-1.816-.354-.54-.852-.972-1.438-1.246.223-.607.27-1.264.14-1.897-.131-.634-.437-1.218-.882-1.687-.47-.445-1.053-.75-1.687-.882-.633-.13-1.29-.083-1.897.14-.273-.587-.704-1.086-1.245-1.44S11.647 1.62 11 1.604c-.646.017-1.273.213-1.813.568s-.969.854-1.24 1.44c-.608-.223-1.267-.272-1.902-.14-.635.13-1.22.436-1.69.882-.445.47-.749 1.055-.878 1.688-.13.633-.08 1.29.144 1.896-.587.274-1.087.705-1.443 1.245-.356.54-.555 1.17-.574 1.817.02.647.218 1.276.574 1.817.356.54.856.972 1.443 1.245-.224.606-.274 1.263-.144 1.896.13.634.433 1.218.877 1.688.47.443 1.054.747 1.687.878.633.132 1.29.084 1.897-.136.274.586.705 1.084 1.246 1.439.54.354 1.17.551 1.816.569.647-.016 1.276-.213 1.817-.567s.972-.854 1.245-1.44c.604.239 1.266.296 1.903.164.636-.132 1.22-.447 1.68-.907.46-.46.776-1.044.908-1.681s.075-1.299-.165-1.903c.586-.274 1.084-.705 1.439-1.246.354-.54.551-1.17.569-1.816zM9.662 14.85l-3.429-3.428 1.293-1.302 2.072 2.072 4.4-4.794 1.347 1.246z">
</path>
</g>
</svg>
<Badge variant="default" class="gap-1">
<BadgeCheck v-if="verified" />
<img v-else-if="icon" :src="icon" alt="" class="size-4 rounded" />
{{ name }}
</Badge>
</TooltipTrigger>
<TooltipContent v-if="description">
<p>{{ description }}</p>
<Text>{{ description }}</Text>
</TooltipContent>
</Tooltip>
</template>
<script lang="ts" setup>
import { BadgeCheck } from "lucide-vue-next";
import { Badge } from "~/components/ui/badge";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "~/components/ui/tooltip";
import Text from "../typography/text.vue";
defineProps<{
name: string;

View file

@ -1,6 +1,5 @@
<template>
<div
class="flex flex-row flex-wrap gap-2 -mx-2"
<Row class="gap-2" wrap
v-if="isDeveloper || account.bot || roles.length > 0"
>
<ProfileBadge
@ -21,13 +20,14 @@
:description="role.description"
:icon="role.icon"
/>
</div>
</Row>
</template>
<script lang="ts" setup>
import type { Account } from "@versia/client/schemas";
import type { z } from "zod";
import * as m from "~/paraglide/messages.js";
import Row from "../typography/layout/row.vue";
import ProfileBadge from "./profile-badge.vue";
const { account } = defineProps<{

View file

@ -1,18 +0,0 @@
<template>
<div :class="['prose prose-sm block relative dark:prose-invert duration-200 !max-w-full break-words prose-a:no-underline prose-a:hover:underline', $style.content]" v-html="content" v-render-emojis="emojis">
</div>
</template>
<script lang="ts" setup>
import type { CustomEmoji } from "@versia/client/schemas";
import type { z } from "zod";
const { content } = defineProps<{
content: string;
emojis: z.infer<typeof CustomEmoji>[];
}>();
</script>
<style module>
@import url("~/styles/content.css");
</style>

View file

@ -1,15 +1,18 @@
<template>
<div class="flex flex-col gap-y-4">
<div v-for="field in fields" :key="field.name" class="flex flex-col gap-1 break-words">
<h3 class="font-semibold text-sm" v-render-emojis="emojis">{{ field.name }}</h3>
<div v-html="field.value" class="prose prose-sm prose-zinc dark:prose-invert" v-render-emojis="emojis"></div>
</div>
</div>
<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>
</template>
<script lang="ts" setup>
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";
defineProps<{
fields: z.infer<typeof Field>[];

View file

@ -0,0 +1,80 @@
<template>
<Button variant="secondary" :disabled="isLoading || relationship?.requested" v-if="!isMe && identity"
@click="relationship?.following ? unfollow() : follow()">
<Loader v-if="isLoading" class="animate-spin" />
<span v-else>
{{
relationship?.following
? m.brief_upper_otter_cuddle()
: relationship?.requested
? m.weak_bright_larva_grasp()
: m.lazy_major_loris_grasp()
}}
</span>
</Button>
</template>
<script lang="ts" setup>
import type { Account } from "@versia/client/schemas";
import { Loader } from "lucide-vue-next";
import { toast } from "vue-sonner";
import type { z } from "zod";
import { Button } from "~/components/ui/button";
import * as m from "~/paraglide/messages.js";
import { confirmModalService } from "../modals/composable";
const { account } = defineProps<{
account: z.infer<typeof Account>;
}>();
const { relationship, isLoading } = useRelationship(client, account.id);
const isMe = identity.value?.account.id === account.id;
const follow = async () => {
if (preferences.confirm_actions.value.includes("follow")) {
const confirmation = await confirmModalService.confirm({
title: m.many_fair_capybara_imagine(),
message: m.mellow_yummy_jannes_cuddle({
acct: `@${account.acct}`,
}),
confirmText: m.cuddly_even_tern_loop(),
cancelText: m.soft_bold_ant_attend(),
});
if (!confirmation.confirmed) {
return;
}
}
const id = toast.loading(m.quick_basic_peacock_bubble());
const { data } = await client.value.followAccount(account.id);
toast.dismiss(id);
relationship.value = data;
toast.success(m.awake_quick_cuckoo_smile());
};
const unfollow = async () => {
if (preferences.confirm_actions.value.includes("follow")) {
const confirmation = await confirmModalService.confirm({
title: m.funny_aloof_swan_loop(),
message: m.white_best_dolphin_catch({
acct: `@${account.acct}`,
}),
confirmText: m.cute_polite_oryx_blend(),
cancelText: m.soft_bold_ant_attend(),
});
if (!confirmation.confirmed) {
return;
}
}
const id = toast.loading(m.big_safe_guppy_mix());
const { data } = await client.value.unfollowAccount(account.id);
toast.dismiss(id);
relationship.value = data;
toast.success(m.misty_level_stingray_expand());
};
</script>

View file

@ -1,41 +1,30 @@
<template>
<div class="flex flex-col gap-2">
<div class="flex flex-row flex-wrap gap-2 *:flex *:items-center *:gap-1 *:text-muted-foreground">
<div>
<CalendarDays class="size-4" />
{{ m.gross_fancy_platypus_seek() }} <span class="text-primary font-semibold">{{ formattedCreationDate }}</span>
</div>
</div>
<div class="flex flex-row flex-wrap gap-2 *:flex *:items-center *:gap-1 *:text-muted-foreground">
<div>
<span class="text-primary font-semibold">{{ noteCount }}</span> {{ m.real_gray_stork_seek() }}
</div>
&middot;
<div>
<span class="text-primary font-semibold">{{ followerCount }}</span> {{ m.teal_helpful_parakeet_hike() }}
</div>
&middot;
<div>
<span class="text-primary font-semibold">{{ followingCount }}</span> {{ m.aloof_royal_samuel_startle() }}
</div>
</div>
</div>
<Row class="gap-2 w-full justify-around">
<Col centered>
<Bold>{{ noteCount }}</Bold>
<Small muted>{{ m.real_gray_stork_seek() }}</Small>
</Col>
<Col centered>
<Bold>{{ followerCount }}</Bold>
<Small muted>{{ m.teal_helpful_parakeet_hike() }}</Small>
</Col>
<Col centered>
<Bold>{{ followingCount }}</Bold>
<Small muted>{{ m.aloof_royal_samuel_startle() }}</Small>
</Col>
</Row>
</template>
<script lang="ts" setup>
import { CalendarDays } from "lucide-vue-next";
import * as m from "~/paraglide/messages.js";
import { getLocale } from "~/paraglide/runtime";
import Bold from "../typography/bold.vue";
import Col from "../typography/layout/col.vue";
import Row from "../typography/layout/row.vue";
import Small from "../typography/small.vue";
const { creationDate } = defineProps<{
creationDate: Date;
const { noteCount, followerCount, followingCount } = defineProps<{
noteCount: number;
followerCount: number;
followingCount: number;
}>();
const formattedCreationDate = new Intl.DateTimeFormat(getLocale(), {
month: "long",
year: "numeric",
}).format(creationDate);
</script>

View file

@ -1,141 +1,65 @@
<template>
<Card class="*:w-full">
<ProfileHeader
:header="account.header"
:avatar="account.avatar"
:display-name="account.display_name"
/>
<CardContent>
<div class="flex flex-row justify-end gap-2">
<Button
variant="secondary"
:disabled="isLoading || relationship?.requested"
v-if="!isMe && identity"
@click="relationship?.following ? unfollow() : follow()"
>
<Loader v-if="isLoading" class="animate-spin" />
<span v-else>
{{
relationship?.following
? m.brief_upper_otter_cuddle()
: relationship?.requested
? m.weak_bright_larva_grasp()
: m.lazy_major_loris_grasp()
}}
</span>
<Card class="gap-4">
<ProfileHeader :header="account.header" :avatar="account.avatar" :display-name="account.display_name" />
<Row class="justify-end gap-2">
<ProfileRelationshipActions :account="account" />
<ProfileActions :account="account">
<Button variant="secondary" size="icon">
<Ellipsis />
</Button>
<ProfileActions :account="account">
<Button variant="secondary" size="icon">
<Ellipsis />
</Button>
</ProfileActions>
</div>
<div class="flex flex-col -mt-1 gap-1 justify-center">
<CardTitle class="" v-render-emojis="account.emojis">
{{ account.display_name }}
</CardTitle>
<CopyableText :text="account.acct">
<span
class="font-semibold bg-gradient-to-tr from-pink-700 dark:from-indigo-400 via-purple-700 dark:via-purple-400 to-indigo-700 dark:to-indigo-400 text-transparent bg-clip-text"
>
@{{ username }}
</span>
<span class="text-muted-foreground"
>{{ instance && "@" }}{{ instance }}</span
>
</CopyableText>
</div>
<ProfileBadges :account="account" class="my-2" />
<ProfileContent :content="account.note" :emojis="account.emojis" />
</CardContent>
<CardFooter class="flex-col *:w-full gap-4">
<ProfileStats
:creation-date="new Date(account.created_at || 0)"
:follower-count="account.followers_count"
:following-count="account.following_count"
:note-count="account.statuses_count"
/>
<Separator v-if="account.fields.length > 0" />
<ProfileFields
v-if="account.fields.length > 0"
:fields="account.fields"
:emojis="account.emojis"
/>
</CardFooter>
</ProfileActions>
</Row>
<Col 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" />
<Row>
<HeadingSmall class="flex items-center gap-1">
<CalendarDays class="size-4" /> {{ formattedCreationDate }}
</HeadingSmall>
</Row>
<Separator />
<ProfileStats :follower-count="account.followers_count" :following-count="account.following_count"
:note-count="account.statuses_count" />
</Card>
</template>
<script lang="ts" setup>
import type { Account } from "@versia/client/schemas";
import { Ellipsis, Loader } from "lucide-vue-next";
import { toast } from "vue-sonner";
import { CalendarDays, Ellipsis } from "lucide-vue-next";
import type { z } from "zod";
import CopyableText from "~/components/notes/copyable-text.vue";
import { Button } from "~/components/ui/button";
import { Card, CardContent, CardFooter, CardTitle } from "~/components/ui/card";
import { Card } from "~/components/ui/card";
import { Separator } from "~/components/ui/separator";
import * as m from "~/paraglide/messages.js";
import { confirmModalService } from "../modals/composable";
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 Row from "../typography/layout/row.vue";
import Text from "../typography/text.vue";
import Address from "./address.vue";
import ProfileActions from "./profile-actions.vue";
import ProfileBadges from "./profile-badges.vue";
import ProfileContent from "./profile-content.vue";
import ProfileFields from "./profile-fields.vue";
import ProfileHeader from "./profile-header.vue";
import ProfileRelationshipActions from "./profile-relationship-actions.vue";
import ProfileStats from "./profile-stats.vue";
const { account } = defineProps<{
account: z.infer<typeof Account>;
}>();
const { relationship, isLoading } = useRelationship(client, account.id);
const isMe = identity.value?.account.id === account.id;
const [username, instance] = account.acct.split("@");
const [username, domain] = account.acct.split("@");
const follow = async () => {
if (preferences.confirm_actions.value.includes("follow")) {
const confirmation = await confirmModalService.confirm({
title: m.many_fair_capybara_imagine(),
message: m.mellow_yummy_jannes_cuddle({
acct: `@${account.acct}`,
}),
confirmText: m.cuddly_even_tern_loop(),
cancelText: m.soft_bold_ant_attend(),
});
if (!confirmation.confirmed) {
return;
}
}
const id = toast.loading(m.quick_basic_peacock_bubble());
const { data } = await client.value.followAccount(account.id);
toast.dismiss(id);
relationship.value = data;
toast.success(m.awake_quick_cuckoo_smile());
};
const unfollow = async () => {
if (preferences.confirm_actions.value.includes("follow")) {
const confirmation = await confirmModalService.confirm({
title: m.funny_aloof_swan_loop(),
message: m.white_best_dolphin_catch({
acct: `@${account.acct}`,
}),
confirmText: m.cute_polite_oryx_blend(),
cancelText: m.soft_bold_ant_attend(),
});
if (!confirmation.confirmed) {
return;
}
}
const id = toast.loading(m.big_safe_guppy_mix());
const { data } = await client.value.unfollowAccount(account.id);
toast.dismiss(id);
relationship.value = data;
toast.success(m.misty_level_stingray_expand());
};
const formattedCreationDate = new Intl.DateTimeFormat(getLocale(), {
dateStyle: "long",
timeStyle: "short",
}).format(new Date(account.created_at || 0));
</script>

View file

@ -23,23 +23,14 @@
</div>
</div>
<div class="flex flex-col justify-center items-center mt-8">
<span class="font-semibold" v-render-emojis="account.emojis">
<Text class="font-bold" v-render-emojis="account.emojis">
{{ account.display_name }}
</span>
<CopyableText :text="account.acct" class="text-sm">
<span
class="font-semibold bg-gradient-to-tr from-pink-700 dark:from-indigo-400 via-purple-700 dark:via-purple-400 to-indigo-700 dark:to-indigo-400 text-transparent bg-clip-text"
>
@{{ username }}
</span>
<span class="text-muted-foreground"
>{{ instance && "@" }}{{ instance }}</span
>
</CopyableText>
</Text>
<Address :username="username" :domain="domain" />
</div>
<ProfileContent
:content="account.note"
:emojis="account.emojis"
<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" />
@ -55,14 +46,15 @@
import type { Account } from "@versia/client/schemas";
import type { z } from "zod";
import { Separator } from "~/components/ui/separator";
import CopyableText from "../notes/copyable-text.vue";
import Html from "../typography/html.vue";
import Text from "../typography/text.vue";
import Address from "./address.vue";
import Avatar from "./avatar.vue";
import ProfileContent from "./profile-content.vue";
import ProfileFields from "./profile-fields.vue";
const { account } = defineProps<{
account: z.infer<typeof Account>;
}>();
const [username, instance] = account.acct.split("@");
const [username, domain] = account.acct.split("@");
</script>

View file

@ -5,14 +5,10 @@
>
<Avatar :src="account.avatar" :name="account.display_name" class="size-10" />
<CardContent class="leading-tight">
<span
class="font-semibold"
v-render-emojis="account.emojis"
>{{ account.display_name }}</span
>
<span class="text-xs">
@{{ account.username }}@{{ domain }}
</span>
<Text class="font-semibold" v-render-emojis="account.emojis">
{{ account.display_name }}
</Text>
<Address :username="account.username" :domain="domain" />
</CardContent>
</Card>
</template>
@ -21,6 +17,8 @@
import type { Account } from "@versia/client/schemas";
import type { z } from "zod";
import { Card, CardContent } from "~/components/ui/card";
import Text from "../typography/text.vue";
import Address from "./address.vue";
import Avatar from "./avatar.vue";
const { account, domain, naked } = defineProps<{

View file

@ -1,12 +1,10 @@
<script setup lang="ts">
import Avatar from "~/components/profiles/avatar.vue";
import InstanceSmallCard from "~/components/instance/small-card.vue";
import {
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "~/components/ui/sidebar";
import * as m from "~/paraglide/messages.js";
const instance = useInstance();
</script>
@ -16,32 +14,7 @@ const instance = useInstance();
<SidebarMenu>
<SidebarMenuItem>
<NuxtLink href="/">
<SidebarMenuButton size="lg">
<Avatar
class="size-8"
:src="
instance?.thumbnail?.url ??
'https://cdn.versia.pub/branding/icon.svg'
"
:name="instance?.title"
/>
<div
class="grid flex-1 text-left text-sm leading-tight"
>
<span class="truncate font-semibold">
{{
instance?.title ??
m.short_zippy_felix_kick()
}}
</span>
<span class="truncate text-xs">
{{
instance?.description ??
m.top_active_ocelot_cure()
}}
</span>
</div>
</SidebarMenuButton>
<InstanceSmallCard v-if="instance" :instance="instance" />
</NuxtLink>
</SidebarMenuItem>
</SidebarMenu>

View file

@ -0,0 +1,23 @@
<script setup lang="ts">
import { Primitive, type PrimitiveProps } from "reka-ui";
import type { HTMLAttributes } from "vue";
import { cn } from "~/lib/utils";
interface Props extends PrimitiveProps {
class?: HTMLAttributes["class"];
}
const props = withDefaults(defineProps<Props>(), {
as: "strong",
});
</script>
<template>
<Primitive
:as="as"
:as-child="asChild"
:class="cn('font-semibold', props.class)"
>
<slot />
</Primitive>
</template>

View file

@ -0,0 +1,23 @@
<script setup lang="ts">
import { Primitive, type PrimitiveProps } from "reka-ui";
import type { HTMLAttributes } from "vue";
import { cn } from "~/lib/utils";
interface Props extends PrimitiveProps {
class?: HTMLAttributes["class"];
}
const props = withDefaults(defineProps<Props>(), {
as: "h1",
});
</script>
<template>
<Primitive
:as="as"
:as-child="asChild"
:class="cn('text-4xl font-extrabold tracking-tight text-balance', props.class)"
>
<slot />
</Primitive>
</template>

View file

@ -0,0 +1,23 @@
<script setup lang="ts">
import { Primitive, type PrimitiveProps } from "reka-ui";
import type { HTMLAttributes } from "vue";
import { cn } from "~/lib/utils";
interface Props extends PrimitiveProps {
class?: HTMLAttributes["class"];
}
const props = withDefaults(defineProps<Props>(), {
as: "h2",
});
</script>
<template>
<Primitive
:as="as"
:as-child="asChild"
:class="cn('text-3xl font-semibold tracking-tight', props.class)"
>
<slot />
</Primitive>
</template>

View file

@ -0,0 +1,23 @@
<script setup lang="ts">
import { Primitive, type PrimitiveProps } from "reka-ui";
import type { HTMLAttributes } from "vue";
import { cn } from "~/lib/utils";
interface Props extends PrimitiveProps {
class?: HTMLAttributes["class"];
}
const props = withDefaults(defineProps<Props>(), {
as: "h3",
});
</script>
<template>
<Primitive
:as="as"
:as-child="asChild"
:class="cn('text-sm font-semibold tracking-tight', props.class)"
>
<slot />
</Primitive>
</template>

View file

@ -0,0 +1,27 @@
<script setup lang="ts">
import { Primitive, type PrimitiveProps } from "reka-ui";
import type { HTMLAttributes } from "vue";
import { cn } from "~/lib/utils";
interface Props extends PrimitiveProps {
class?: HTMLAttributes["class"];
}
const props = withDefaults(defineProps<Props>(), {
as: "div",
});
</script>
<template>
<Primitive
:as="as"
:as-child="asChild"
:class="cn('prose prose-sm block relative dark:prose-invert duration-200 !max-w-full break-words prose-a:no-underline prose-a:hover:underline', $style.content, props.class)"
>
<slot />
</Primitive>
</template>
<style module>
@import url("~/styles/content.css");
</style>

View file

@ -0,0 +1,25 @@
<script setup lang="ts">
import { Primitive, type PrimitiveProps } from "reka-ui";
import type { HTMLAttributes } from "vue";
import { cn } from "~/lib/utils";
interface Props extends PrimitiveProps {
class?: HTMLAttributes["class"];
wrap?: boolean;
centered?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
as: "div",
});
</script>
<template>
<Primitive
:as="as"
:as-child="asChild"
:class="cn('flex flex-col', props.wrap && 'flex-wrap', props.class, props.centered && 'items-center')"
>
<slot />
</Primitive>
</template>

View file

@ -0,0 +1,25 @@
<script setup lang="ts">
import { Primitive, type PrimitiveProps } from "reka-ui";
import type { HTMLAttributes } from "vue";
import { cn } from "~/lib/utils";
interface Props extends PrimitiveProps {
class?: HTMLAttributes["class"];
wrap?: boolean;
centered?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
as: "div",
});
</script>
<template>
<Primitive
:as="as"
:as-child="asChild"
:class="cn('flex flex-row', props.wrap && 'flex-wrap', props.class, props.centered && 'items-center')"
>
<slot />
</Primitive>
</template>

View file

@ -0,0 +1,24 @@
<script setup lang="ts">
import { Primitive, type PrimitiveProps } from "reka-ui";
import type { HTMLAttributes } from "vue";
import { cn } from "~/lib/utils";
interface Props extends PrimitiveProps {
class?: HTMLAttributes["class"];
muted?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
as: "span",
});
</script>
<template>
<Primitive
:as="as"
:as-child="asChild"
:class="cn('text-xs', props.class, props.muted && 'text-muted-foreground')"
>
<slot />
</Primitive>
</template>

View file

@ -0,0 +1,24 @@
<script setup lang="ts">
import { Primitive, type PrimitiveProps } from "reka-ui";
import type { HTMLAttributes } from "vue";
import { cn } from "~/lib/utils";
interface Props extends PrimitiveProps {
class?: HTMLAttributes["class"];
muted?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
as: "p",
});
</script>
<template>
<Primitive
:as="as"
:as-child="asChild"
:class="cn('leading-7', props.class, props.muted && 'text-muted-foreground')"
>
<slot />
</Primitive>
</template>

View file

@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1744536153,
"narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
"lastModified": 1751949589,
"narHash": "sha256-mgFxAPLWw0Kq+C8P3dRrZrOYEQXOtKuYVlo9xvPntt8=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
"rev": "9b008d60392981ad674e04016d25619281550a9d",
"type": "github"
},
"original": {

View file

@ -17,7 +17,7 @@ in
pnpmDeps = pnpm.fetchDeps {
inherit (finalAttrs) pname version src;
hash = "sha256-vNx9MNsMqllLxRxC93slvzHza6cHtPKgWxbvTy6QH4M=";
hash = "sha256-or+GR3zZGOqcC/6h2pHlhK2SGyysIAnghik8gpJhAlk=";
};
nativeBuildInputs = [

View file

@ -37,26 +37,26 @@
"@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.1.11",
"@tanstack/vue-table": "^8.21.3",
"@tiptap/extension-highlight": "^2.22.3",
"@tiptap/extension-image": "^2.22.3",
"@tiptap/extension-link": "^2.22.3",
"@tiptap/extension-mention": "^2.22.3",
"@tiptap/extension-placeholder": "^2.22.3",
"@tiptap/extension-subscript": "^2.22.3",
"@tiptap/extension-superscript": "^2.22.3",
"@tiptap/extension-task-item": "^2.22.3",
"@tiptap/extension-task-list": "^2.22.3",
"@tiptap/extension-underline": "^2.22.3",
"@tiptap/pm": "^2.22.3",
"@tiptap/starter-kit": "^2.22.3",
"@tiptap/suggestion": "^2.22.3",
"@tiptap/vue-3": "^2.22.3",
"@tiptap/extension-highlight": "^2.25.0",
"@tiptap/extension-image": "^2.25.0",
"@tiptap/extension-link": "^2.25.0",
"@tiptap/extension-mention": "^2.25.0",
"@tiptap/extension-placeholder": "^2.25.0",
"@tiptap/extension-subscript": "^2.25.0",
"@tiptap/extension-superscript": "^2.25.0",
"@tiptap/extension-task-item": "^2.25.0",
"@tiptap/extension-task-list": "^2.25.0",
"@tiptap/extension-underline": "^2.25.0",
"@tiptap/pm": "^2.25.0",
"@tiptap/starter-kit": "^2.25.0",
"@tiptap/suggestion": "^2.25.0",
"@tiptap/vue-3": "^2.25.0",
"@vee-validate/zod": "^4.15.1",
"@versia/client": "0.2.0-alpha.4",
"@videojs-player/vue": "^1.0.0",
"@vite-pwa/nuxt": "^1.0.4",
"@vueuse/core": "^13.4.0",
"@vueuse/nuxt": "^13.4.0",
"@vueuse/core": "^13.5.0",
"@vueuse/nuxt": "^13.5.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"embla-carousel-vue": "^8.6.0",
@ -64,18 +64,18 @@
"emojibase-data": "^16.0.3",
"fuzzysort": "^3.1.0",
"html-to-text": "^9.0.5",
"lucide-vue-next": "^0.523.0",
"lucide-vue-next": "^0.525.0",
"magic-regexp": "^0.10.0",
"mitt": "^3.0.1",
"nanoid": "^5.1.5",
"nuxt": "^3.17.5",
"nuxt": "^3.17.6",
"nuxt-security": "^2.2.0",
"reka-ui": "^2.3.1",
"reka-ui": "^2.3.2",
"shadcn-nuxt": "2.2.0",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.11",
"tailwindcss-animate": "^1.0.7",
"tw-animate-css": "^1.3.4",
"tw-animate-css": "^1.3.5",
"vaul-vue": "^0.4.1",
"vee-validate": "^4.15.1",
"virtua": "^0.41.5",
@ -86,19 +86,19 @@
"zod": "^3.25.67"
},
"devDependencies": {
"@biomejs/biome": "^2.0.5",
"@biomejs/biome": "^2.1.1",
"@iconify-json/fluent-emoji": "^1.2.3",
"@iconify-json/fluent-emoji-flat": "^1.2.3",
"@iconify-json/noto": "^1.2.3",
"@iconify-json/twemoji": "^1.2.2",
"@iconify/utils": "^2.3.0",
"@inlang/paraglide-js": "2.1.0",
"@inlang/paraglide-js": "2.2.0",
"@inlang/plugin-m-function-matcher": "^2.1.0",
"@inlang/plugin-message-format": "^4.0.0",
"@tailwindcss/forms": "^0.5.10",
"@types/html-to-text": "^9.0.4",
"typescript": "^5.8.3",
"vue-tsc": "^2.2.10"
"vue-tsc": "^3.0.1"
},
"trustedDependencies": [
"@biomejs/biome",

File diff suppressed because it is too large Load diff

View file

@ -6,78 +6,72 @@
@custom-variant dark (&:is(.dark *));
:root {
--radius: 0.65rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--warning: oklch(0.577 0.245 27.325);
--warning-foreground: oklch(0.577 0.245 27.325);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.705 0.015 286.067);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.705 0.015 286.067);
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.141 0.005 285.823);
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.141 0.005 285.823);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.141 0.005 285.823);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.21 0.006 285.885);
--secondary: oklch(0.274 0.006 286.033);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--warning: oklch(0.396 0.141 25.723);
--warning-foreground: oklch(0.396 0.141 25.723);
--destructive: oklch(0.396 0.141 25.723);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(0.274 0.006 286.033);
--input: oklch(0.274 0.006 286.033);
--ring: oklch(0.442 0.017 285.786);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.21 0.006 285.885);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.274 0.006 286.033);
--sidebar-ring: oklch(0.442 0.017 285.786);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@theme inline {