mirror of
https://github.com/versia-pub/frontend.git
synced 2026-01-26 04:16:02 +01:00
refactor: ♻️ Redesign note UI
This commit is contained in:
parent
3627ac0ef8
commit
35f72e6197
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<Button variant="ghost" class="max-w-14 w-full" size="sm">
|
<Button variant="ghost" size="sm">
|
||||||
<component :is="icon" class="size-4"/>
|
<component :is="icon" class="size-4"/>
|
||||||
<slot/>
|
<slot/>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-row w-full max-w-sm items-stretch justify-between">
|
<div class="flex items-center gap-1">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
:icon="Reply"
|
:icon="Reply"
|
||||||
@click="emit('reply')"
|
@click="emit('reply')"
|
||||||
|
|
@ -39,21 +39,6 @@
|
||||||
:disabled="!authStore.isSignedIn"
|
:disabled="!authStore.isSignedIn"
|
||||||
/>
|
/>
|
||||||
</Picker>
|
</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>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,64 +1,43 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="rounded grid grid-cols-[auto_1fr_auto] items-center gap-3">
|
<div class="flex items-start justify-between">
|
||||||
<HoverCard
|
<div class="flex items-center gap-3">
|
||||||
v-model:open="popupOpen"
|
<NuxtLink :href="urlAsPath">
|
||||||
@update:open="() => {
|
<Avatar :src="author.avatar" :name="author.display_name"/>
|
||||||
if (!preferences.popup_avatar_hover) {
|
|
||||||
popupOpen = false;
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
: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>
|
</NuxtLink>
|
||||||
</HoverCardTrigger>
|
<div class="flex flex-col gap-0.5">
|
||||||
<HoverCardContent class="w-96">
|
<div class="flex items-center gap-1">
|
||||||
<SmallCard :account="author"/>
|
<span
|
||||||
</HoverCardContent>
|
class="text-sm font-semibold"
|
||||||
</HoverCard>
|
v-render-emojis="author.emojis"
|
||||||
<Column :class="smallLayout && 'text-sm'">
|
>{{ author.display_name }}</span
|
||||||
<Text class="font-semibold" v-render-emojis="author.emojis">
|
|
||||||
{{
|
|
||||||
author.display_name
|
|
||||||
}}
|
|
||||||
</Text>
|
|
||||||
<div class="-mt-1">
|
|
||||||
<Address as="span" :username="username" :domain="instance"/>
|
|
||||||
·
|
|
||||||
<Text
|
|
||||||
as="span"
|
|
||||||
muted
|
|
||||||
class="ml-auto tracking-normal"
|
|
||||||
:title="fullTime"
|
|
||||||
>
|
>
|
||||||
{{ timeAgo }}
|
|
||||||
</Text>
|
|
||||||
</div>
|
</div>
|
||||||
</Column>
|
<div
|
||||||
<div v-if="!smallLayout">
|
class="flex items-center gap-1 text-muted-foreground text-xs"
|
||||||
<NuxtLink
|
|
||||||
:href="noteUrlAsPath"
|
|
||||||
class="text-xs text-muted-foreground"
|
|
||||||
:title="visibilities[visibility].text"
|
|
||||||
>
|
>
|
||||||
<component :is="visibilities[visibility].icon" class="size-4"/>
|
<span>
|
||||||
</NuxtLink>
|
@{{ `${username}${instance ? `@${instance}` : ""}` }}
|
||||||
|
</span>
|
||||||
|
<span>·</span>
|
||||||
|
<span>{{ timeAgo }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<Menu
|
||||||
|
:api-note-string="apiNoteString"
|
||||||
|
:url="noteUrl"
|
||||||
|
:remote-url="remoteUrl"
|
||||||
|
:is-remote="isRemote"
|
||||||
|
:author-id="author.id"
|
||||||
|
@edit="emit('edit')"
|
||||||
|
:note-id="noteId"
|
||||||
|
@delete="emit('delete')"
|
||||||
|
>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<Ellipsis/>
|
||||||
|
</Button>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
|
@ -67,29 +46,24 @@ import type {
|
||||||
UseTimeAgoMessages,
|
UseTimeAgoMessages,
|
||||||
UseTimeAgoUnitNamesDefault,
|
UseTimeAgoUnitNamesDefault,
|
||||||
} from "@vueuse/core";
|
} from "@vueuse/core";
|
||||||
import { AtSign, Globe, Lock, LockOpen } from "lucide-vue-next";
|
import { AtSign, Ellipsis, Globe, Lock, LockOpen } from "lucide-vue-next";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { getLocale } from "~~/paraglide/runtime";
|
import { getLocale } from "~~/paraglide/runtime";
|
||||||
import Address from "../profiles/address.vue";
|
|
||||||
import Avatar from "../profiles/avatar.vue";
|
import Avatar from "../profiles/avatar.vue";
|
||||||
import SmallCard from "../profiles/small-card.vue";
|
import { Button } from "../ui/button";
|
||||||
import Column from "../typography/layout/col.vue";
|
import Menu from "./menu.vue";
|
||||||
import Text from "../typography/text.vue";
|
|
||||||
import {
|
|
||||||
HoverCard,
|
|
||||||
HoverCardContent,
|
|
||||||
HoverCardTrigger,
|
|
||||||
} from "../ui/hover-card";
|
|
||||||
|
|
||||||
const { createdAt, noteUrl, author, authorUrl } = defineProps<{
|
const { createdAt, noteUrl, author, authorUrl } = defineProps<{
|
||||||
cornerAvatar?: string;
|
cornerAvatar?: string;
|
||||||
visibility: z.infer<typeof Status.shape.visibility>;
|
visibility: z.infer<typeof Status.shape.visibility>;
|
||||||
noteUrl: string;
|
noteUrl: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
smallLayout?: boolean;
|
|
||||||
author: z.infer<typeof Account>;
|
author: z.infer<typeof Account>;
|
||||||
authorUrl: string;
|
authorUrl: string;
|
||||||
|
remoteUrl?: string;
|
||||||
|
apiNoteString: string;
|
||||||
|
isRemote: boolean;
|
||||||
|
noteId: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const [username, instance] = author.acct.split("@");
|
const [username, instance] = author.acct.split("@");
|
||||||
|
|
@ -117,6 +91,11 @@ const fullTime = new Intl.DateTimeFormat(getLocale(), {
|
||||||
}).format(createdAt);
|
}).format(createdAt);
|
||||||
const popupOpen = ref(false);
|
const popupOpen = ref(false);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
edit: [];
|
||||||
|
delete: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
const visibilities = {
|
const visibilities = {
|
||||||
public: {
|
public: {
|
||||||
icon: Globe,
|
icon: Globe,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<Card as="article" class="relative gap-3 items-stretch">
|
<Card
|
||||||
|
as="article"
|
||||||
|
:class="['relative gap-1.5 items-stretch bg-background', replyBar && 'pl-6']"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="replyBar"
|
||||||
|
class="absolute left-0 top-0 bottom-0 w-2 bg-border rounded-tl-md"
|
||||||
|
/>
|
||||||
<CardHeader as="header" class="space-y-2">
|
<CardHeader as="header" class="space-y-2">
|
||||||
<ReblogHeader
|
<ReblogHeader
|
||||||
v-if="note.reblog"
|
v-if="note.reblog"
|
||||||
|
|
@ -13,34 +20,18 @@
|
||||||
:author-url="accountUrl"
|
:author-url="accountUrl"
|
||||||
:corner-avatar="note.reblog ? note.account.avatar : undefined"
|
:corner-avatar="note.reblog ? note.account.avatar : undefined"
|
||||||
:note-url="url"
|
:note-url="url"
|
||||||
|
:is-remote="isRemote"
|
||||||
|
:remote-url="noteToUse.url ?? undefined"
|
||||||
|
:api-note-string="JSON.stringify(noteToUse, null, 4)"
|
||||||
:visibility="noteToUse.visibility"
|
:visibility="noteToUse.visibility"
|
||||||
:created-at="new Date(noteToUse.created_at)"
|
:created-at="new Date(noteToUse.created_at)"
|
||||||
:small-layout="smallLayout"
|
@edit="useEvent('composer:edit', noteToUse)"
|
||||||
|
@delete="useEvent('note:delete', noteToUse)"
|
||||||
|
:note-id="noteToUse.id"
|
||||||
class="z-1"
|
class="z-1"
|
||||||
/>
|
/>
|
||||||
<div
|
|
||||||
v-if="topAvatarBar"
|
|
||||||
:class="
|
|
||||||
cn(
|
|
||||||
'shrink-0 bg-border w-0.5 absolute top-0 h-7 left-12'
|
|
||||||
)
|
|
||||||
"
|
|
||||||
></div>
|
|
||||||
<div
|
|
||||||
v-if="bottomAvatarBar"
|
|
||||||
:class="
|
|
||||||
cn(
|
|
||||||
'shrink-0 bg-border w-0.5 absolute bottom-0 h-[calc(100%-1.5rem)] left-12'
|
|
||||||
)
|
|
||||||
"
|
|
||||||
></div>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<!-- Simply offset by the size of avatar + 0.75rem (the gap) -->
|
<CardContent class="space-y-2">
|
||||||
<CardContent
|
|
||||||
:class="
|
|
||||||
['space-y-4', contentUnderUsername && (smallLayout ? 'ml-11' : 'ml-17')]
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<Content
|
<Content
|
||||||
:content="noteToUse.content"
|
:content="noteToUse.content"
|
||||||
:quote="note.quote ?? undefined"
|
:quote="note.quote ?? undefined"
|
||||||
|
|
@ -82,7 +73,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Status } from "@versia/client/schemas";
|
import type { Status } from "@versia/client/schemas";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { Card, CardContent, CardFooter, CardHeader } from "../ui/card";
|
import { Card, CardContent, CardFooter, CardHeader } from "../ui/card";
|
||||||
import Actions from "./actions.vue";
|
import Actions from "./actions.vue";
|
||||||
import Content from "./content.vue";
|
import Content from "./content.vue";
|
||||||
|
|
@ -92,13 +82,14 @@ import ReblogHeader from "./reblog-header.vue";
|
||||||
|
|
||||||
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
||||||
|
|
||||||
const { note } = defineProps<{
|
const {
|
||||||
|
note,
|
||||||
|
hideActions,
|
||||||
|
replyBar = false,
|
||||||
|
} = defineProps<{
|
||||||
note: PartialBy<z.infer<typeof Status>, "reblog" | "quote">;
|
note: PartialBy<z.infer<typeof Status>, "reblog" | "quote">;
|
||||||
hideActions?: boolean;
|
hideActions?: boolean;
|
||||||
smallLayout?: boolean;
|
replyBar?: boolean;
|
||||||
contentUnderUsername?: boolean;
|
|
||||||
topAvatarBar?: boolean;
|
|
||||||
bottomAvatarBar?: boolean;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// Notes can be reblogs, in which case the actual thing to render is inside the reblog property
|
// Notes can be reblogs, in which case the actual thing to render is inside the reblog property
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-row gap-2 flex-wrap">
|
<div class="flex flex-row gap-1 flex-wrap">
|
||||||
<Reaction
|
<Reaction
|
||||||
v-for="reaction in reactions"
|
v-for="reaction in reactions"
|
||||||
:key="reaction.name"
|
:key="reaction.name"
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,9 @@
|
||||||
v-if="emoji"
|
v-if="emoji"
|
||||||
:src="emoji.url"
|
:src="emoji.url"
|
||||||
:alt="emoji.shortcode"
|
:alt="emoji.shortcode"
|
||||||
class="h-[1lh] align-middle inline not-prose"
|
class="h-lh align-middle inline not-prose"
|
||||||
>
|
>
|
||||||
<span v-else> {{ reaction.name }}</span>
|
<span v-else>{{ reaction.name }}</span>
|
||||||
{{ formatNumber(reaction.count) }}
|
{{ formatNumber(reaction.count) }}
|
||||||
</Button>
|
</Button>
|
||||||
</HoverCardTrigger>
|
</HoverCardTrigger>
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,10 @@
|
||||||
v-if="parent"
|
v-if="parent"
|
||||||
:note="parent"
|
:note="parent"
|
||||||
:hide-actions="true"
|
:hide-actions="true"
|
||||||
:content-under-username="true"
|
:reply-bar="true"
|
||||||
:bottom-avatar-bar="true"
|
class="rounded-b-none"
|
||||||
class="border-b-0 rounded-b-none"
|
|
||||||
/>
|
|
||||||
<Note
|
|
||||||
:note="note"
|
|
||||||
:class="parent && 'border-t-0 rounded-t-none'"
|
|
||||||
:top-avatar-bar="!!parent"
|
|
||||||
/>
|
/>
|
||||||
|
<Note :note="note" :class="parent && 'border-t-0 rounded-t-none'"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue