fix: 🐛 Fix broken Note UIs

This commit is contained in:
Jesse Wierzbinski 2025-04-10 14:48:03 +02:00
parent b6080eff60
commit ac0a571ecc
No known key found for this signature in database
20 changed files with 154 additions and 95 deletions

View file

@ -67,7 +67,6 @@
<Button variant="ghost" size="icon">
<component
:is="visibilities[state.visibility].icon"
class="!size-5"
/>
</Button>
</SelectTrigger>

View file

@ -0,0 +1,15 @@
<template>
<Button variant="ghost" class="max-w-14 w-full">
<component :is="icon" class="size-4.5" />
<slot />
</Button>
</template>
<script lang="ts" setup>
import type { FunctionalComponent } from "vue";
import { Button } from "../ui/button";
const { icon } = defineProps<{
icon: FunctionalComponent;
}>();
</script>

View file

@ -1,24 +1,17 @@
<template>
<div class="flex flex-row w-full gap-x-6 items-stretch justify-start text-sm *:max-w-14 *:w-full *:text-muted-foreground">
<Button variant="ghost" @click="emit('reply')" :title="m.drab_tense_turtle_comfort()" :disabled="!identity">
<Reply class="size-5 text-primary" />
<div class="flex flex-row w-full gap-x-4 items-stretch justify-start">
<ActionButton :icon="Reply" @click="emit('reply')" :title="m.drab_tense_turtle_comfort()" :disabled="!identity">
{{ numberFormat(replyCount) }}
</Button>
<Button variant="ghost" @click="liked ? unlike() : like()" :title="liked ? m.vexed_fluffy_clownfish_dance() : m.royal_close_samuel_scold()" :disabled="!identity" :class="liked && '*:fill-red-600 *:text-red-600'">
<Heart class="size-5 text-primary" />
</ActionButton>
<ActionButton :icon="Heart" @click="liked ? unlike() : like()" :title="liked ? m.vexed_fluffy_clownfish_dance() : m.royal_close_samuel_scold()" :disabled="!identity" :class="liked && '*:fill-red-600 *:text-red-600'">
{{ numberFormat(likeCount) }}
</Button>
<Button variant="ghost" @click="reblogged ? unreblog() : reblog()" :title="reblogged ? m.lime_neat_ox_stab() : m.aware_helpful_marlin_drop()" :disabled="!identity" :class="reblogged && '*:text-green-600'">
<Repeat class="size-5 text-primary" />
</ActionButton>
<ActionButton :icon="Repeat" @click="reblogged ? unreblog() : reblog()" :title="reblogged ? m.lime_neat_ox_stab() : m.aware_helpful_marlin_drop()" :disabled="!identity" :class="reblogged && '*:text-green-600'">
{{ numberFormat(reblogCount) }}
</Button>
<Button variant="ghost" @click="emit('quote')" :title="m.true_shy_jackal_drip()" :disabled="!identity">
<Quote class="size-5 text-primary" />
</Button>
</ActionButton>
<ActionButton :icon="Quote" @click="emit('quote')" :title="m.true_shy_jackal_drip()" :disabled="!identity" />
<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')">
<Button variant="ghost" :title="m.busy_merry_cowfish_absorb()">
<Ellipsis class="size-5 text-primary" />
</Button>
<ActionButton :icon="Ellipsis" :title="m.busy_merry_cowfish_absorb()" />
</Menu>
</div>
</template>
@ -26,11 +19,11 @@
<script lang="ts" setup>
import { Ellipsis, Heart, Quote, Repeat, Reply } from "lucide-vue-next";
import { toast } from "vue-sonner";
import { Button } from "~/components/ui/button";
import * as m from "~/paraglide/messages.js";
import { getLocale } from "~/paraglide/runtime";
import { SettingIds } from "~/settings";
import { confirmModalService } from "../modals/composable";
import ActionButton from "./action-button.vue";
import Menu from "./menu.vue";
const { noteId } = defineProps<{

View file

@ -1,6 +1,6 @@
<template>
<!-- [&:has(>:last-child:nth-child(1))] means "when this element has 1 child" -->
<div class="mt-4 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">
<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

@ -0,0 +1,26 @@
<template>
<Alert layout="button">
<TriangleAlert />
<AlertTitle class="sr-only">{{ m.livid_tangy_lionfish_clasp() }}</AlertTitle>
<AlertDescription>
{{ contentWarning || m.sour_seemly_bird_hike() }}
</AlertDescription>
<Button @click="blurred = !blurred" variant="outline" size="sm">{{ blurred ? m.bald_direct_turtle_win() :
m.known_flaky_cockroach_dash() }}</Button>
</Alert>
</template>
<script lang="ts" setup>
import { TriangleAlert } from "lucide-vue-next";
import * as m from "~/paraglide/messages.js";
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
import { Button } from "../ui/button";
const { contentWarning } = defineProps<{
contentWarning?: string;
}>();
const blurred = defineModel<boolean>({
default: true,
});
</script>

View file

@ -1,34 +1,9 @@
<template>
<Alert variant="warning" v-if="(sensitive || contentWarning) && showCw.value"
class="mb-4 py-2 px-4 grid grid-cols-[auto,1fr,auto] gap-2 items-center [&>svg~*]:pl-0 [&>svg+div]:translate-y-0 [&>svg]:static">
<AlertTitle class="sr-only">{{ m.livid_tangy_lionfish_clasp() }}</AlertTitle>
<div>
<TriangleAlert class="size-4" />
</div>
<AlertDescription>
{{ contentWarning || m.sour_seemly_bird_hike() }}
</AlertDescription>
<Button @click="blurred = !blurred" variant="outline" size="sm">{{ blurred ? m.bald_direct_turtle_win() : m.known_flaky_cockroach_dash() }}</Button>
</Alert>
<ContentWarning v-if="(sensitive || contentWarning) && showCw.value" :content-warning="contentWarning" v-model="blurred" />
<div ref="container" :class="cn('overflow-y-hidden relative duration-200', (blurred && showCw.value) && 'blur-md')" :style="{
maxHeight: collapsed ? '18rem' : `${container?.scrollHeight}px`,
}">
<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,
]" v-html="content" v-render-emojis="emojis"></div>
<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()}${plainContent ? `${m.dark_spare_goldfish_charm({
count: formattedCharacterCount ?? '0',
})}` : "" }`
: m.that_misty_mule_arrive()
}}</Button>
</div>
<OverflowGuard :character-count="characterCount" :class="(blurred && showCw.value) && 'blur-md'">
<Prose v-html="content" v-render-emojis="emojis"></Prose>
</OverflowGuard>
<Attachments v-if="attachments.length > 0" :attachments="attachments" :class="(blurred && showCw.value) && 'blur-xl'" />
@ -38,16 +13,13 @@
</template>
<script lang="ts" setup>
import { cn } from "@/lib/utils";
import type { Attachment, Emoji, Status } from "@versia/client/types";
import { TriangleAlert } from "lucide-vue-next";
import { Button } from "~/components/ui/button";
import * as m from "~/paraglide/messages.js";
import { getLocale } from "~/paraglide/runtime";
import { type BooleanSetting, SettingIds } from "~/settings";
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
import Attachments from "./attachments.vue";
import ContentWarning from "./content-warning.vue";
import Note from "./note.vue";
import OverflowGuard from "./overflow-guard.vue";
import Prose from "./prose.vue";
const { content, plainContent, sensitive, contentWarning } = defineProps<{
plainContent?: string;
@ -58,30 +30,9 @@ const { content, plainContent, sensitive, contentWarning } = defineProps<{
sensitive: boolean;
contentWarning?: string;
}>();
const container = ref<HTMLDivElement | null>(null);
const collapsed = ref(true);
const blurred = ref(sensitive || !!contentWarning);
const showCw = useSetting(SettingIds.ShowContentWarning) as Ref<BooleanSetting>;
// max-h-72 is 18rem
const remToPx = (rem: number) =>
rem *
Number.parseFloat(
getComputedStyle(document.documentElement).fontSize || "16px",
);
const isOverflowing = computed(() => {
if (!container.value) {
return false;
}
return container.value.scrollHeight > remToPx(18);
});
const characterCount = plainContent?.length;
const formattedCharacterCount = characterCount
? new Intl.NumberFormat(getLocale()).format(characterCount)
: undefined;
</script>
<style module>
@import url("~/styles/content.css");
</style>

View file

@ -22,13 +22,13 @@
author.display_name
}}</span>
<span class="truncate text-sm tracking-tight">
<CopyableText :text="author.acct">
<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>
</CopyableText>
</span>
&middot;
<span class="text-muted-foreground ml-auto tracking-normal" :title="fullTime">{{ timeAgo }}</span>
</span>

View file

@ -1,6 +1,6 @@
<template>
<Card as="article" class="relative gap-4 items-stretch">
<CardHeader as="header">
<CardHeader as="header" class="space-y-2">
<ReblogHeader
v-if="note.reblog"
:avatar="note.account.avatar"
@ -38,7 +38,7 @@
<!-- Simply offset by the size of avatar + 0.75rem (the gap) -->
<CardContent
:class="
contentUnderUsername && (smallLayout ? 'ml-11' : 'ml-[4.25rem]')
['space-y-4', contentUnderUsername && (smallLayout ? 'ml-11' : 'ml-[4.25rem]')]
"
>
<Content

View file

@ -0,0 +1,49 @@
<template>
<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">{{
collapsed
? `${m.lazy_honest_mammoth_bump()}${formattedCharacterCount ? `${m.dark_spare_goldfish_charm({
count: formattedCharacterCount,
})}` : ""}`
: m.that_misty_mule_arrive()
}}</Button>
</div>
</template>
<script lang="ts" setup>
import * as m from "~/paraglide/messages.js";
import { getLocale } from "~/paraglide/runtime";
import { Button } from "../ui/button";
const { characterCount = 0 } = defineProps<{
characterCount?: number;
}>();
const container = useTemplateRef<HTMLDivElement>("container");
const collapsed = ref(true);
// max-h-72 is 18rem
const remToPx = (rem: number) =>
rem *
Number.parseFloat(
getComputedStyle(document.documentElement).fontSize || "16px",
);
const isOverflowing = computed(() => {
if (!container.value) {
return false;
}
return container.value.scrollHeight > remToPx(18);
});
const formattedCharacterCount =
characterCount > 0
? new Intl.NumberFormat(getLocale()).format(characterCount)
: undefined;
</script>

View file

@ -0,0 +1,12 @@
<template>
<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 />
</div>
</template>
<style module>
@import "~/styles/content.css";
</style>

View file

@ -1,9 +1,11 @@
<template>
<NuxtLink :href="urlAsPath" class="rounded border hover:bg-muted duration-100 text-sm flex flex-row items-center gap-2 px-2 py-1 mb-4">
<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() }}
<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>
{{ m.large_vivid_horse_catch() }}
</Card>
</NuxtLink>
</template>
@ -12,6 +14,7 @@ import type { Emoji } from "@versia/client/types";
import { Repeat } from "lucide-vue-next";
import * as m from "~/paraglide/messages.js";
import Avatar from "../profiles/avatar.vue";
import { Card } from "../ui/card";
const { url } = defineProps<{
avatar: string;

View file

@ -1,5 +1,5 @@
<template>
<Avatar :shape="(shape.value as 'circle' | 'square')" :size="size">
<Avatar :class="shape.value === 'square' && 'rounded-md'" :size="size">
<AvatarFallback v-if="name">
{{ getInitials(name) }}
</AvatarFallback>

View file

@ -6,13 +6,14 @@ import { type AlertVariants, alertVariants } from ".";
const props = defineProps<{
class?: HTMLAttributes["class"];
variant?: AlertVariants["variant"];
layout?: AlertVariants["layout"];
}>();
</script>
<template>
<div
data-slot="alert"
:class="cn(alertVariants({ variant }), props.class)"
:class="cn(alertVariants({ variant, layout }), props.class)"
role="alert"
>
<slot />

View file

@ -10,7 +10,7 @@ const props = defineProps<{
<template>
<div
data-slot="alert-description"
:class="cn('text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed', props.class)"
:class="cn('text-muted-foreground text-sm [&_p]:leading-relaxed', props.class)"
>
<slot />
</div>

View file

@ -10,7 +10,7 @@ const props = defineProps<{
<template>
<div
data-slot="alert-title"
:class="cn('col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight', props.class)"
:class="cn('line-clamp-1 min-h-4 font-medium tracking-tight', props.class)"
>
<slot />
</div>

View file

@ -5,7 +5,7 @@ export { default as AlertDescription } from "./AlertDescription.vue";
export { default as AlertTitle } from "./AlertTitle.vue";
export const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
"relative w-full rounded-lg border px-4 py-3 grid text-sm [&>svg]:size-4 [&>svg]:text-current",
{
variants: {
variant: {
@ -13,9 +13,15 @@ export const alertVariants = cva(
destructive:
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
},
layout: {
default:
"has-[>svg]:grid-cols-[1fr_auto] grid-rows-2 gap-x-3 gap-y-1 items-start",
button: "grid-cols-[auto_1fr_auto] items-center gap-x-3 gap-y-0.5",
},
},
defaultVariants: {
variant: "default",
layout: "default",
},
},
);

View file

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

View file

@ -13,7 +13,10 @@ import { type HTMLAttributes, computed } from "vue";
import DialogOverlay from "./DialogOverlay.vue";
const props = defineProps<
DialogContentProps & { class?: HTMLAttributes["class"] }
DialogContentProps & {
class?: HTMLAttributes["class"];
hideClose?: boolean;
}
>();
const emits = defineEmits<DialogContentEmits>();
@ -41,6 +44,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
<slot />
<DialogClose
v-if="!hideClose"
class="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<X />

View file

@ -32,7 +32,7 @@ const forwardedProps = useForwardProps(delegatedProps);
:data-inset="inset ? '' : undefined"
:data-variant="variant"
v-bind="forwardedProps"
:class="cn(`focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive-foreground data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/40 data-[variant=destructive]:focus:text-destructive-foreground data-[variant=destructive]:*:[svg]:!text-destructive-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`, props.class)"
:class="cn(`focus:bg-accent w-full focus:text-accent-foreground data-[variant=destructive]:text-destructive-foreground data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/40 data-[variant=destructive]:focus:text-destructive-foreground data-[variant=destructive]:*:[svg]:!text-destructive-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`, props.class)"
>
<slot />
</DropdownMenuItem>

View file

@ -12,7 +12,7 @@
:bottom-avatar-bar="true"
:content-under-username="true"
/>
<Note v-if="note" :note="note" :top-avatar-bar="true" />
<Note v-if="note" :note="note" :top-avatar-bar="(context?.ancestors.length ?? 0) > 0" />
</div>
<Note v-for="note of context?.descendants" :note="note" />
</div>