Skip to content

Commit c2ccd86

Browse files
committed
fix(blog): UI improvements and Instagram-style comment redesign (#2136, #2137)
Redesign nested comments to use a flat, single-indent layout with collapsible reply threads. Fix blog content alignment, button hover states, Bluesky embed icon positioning, prose link hover effects, and federated articles shadow/alignment.
1 parent c5202d0 commit c2ccd86

File tree

7 files changed

+110
-49
lines changed

7 files changed

+110
-49
lines changed

app/components/BlueskyComment.vue

Lines changed: 10 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,10 @@ function getCommentUrl(comment: Comment): string {
1313
}
1414
const props = defineProps<{
1515
comment: Comment
16-
depth: number
16+
isReply?: boolean
17+
replyingTo?: string
1718
}>()
1819
19-
const MaxDepth = 4
20-
2120
function getFeatureUrl(feature: RichtextFeature): string | undefined {
2221
if (feature.$type === 'app.bsky.richtext.facet#link') return feature.uri
2322
if (feature.$type === 'app.bsky.richtext.facet#mention')
@@ -41,7 +40,7 @@ function getHostname(uri: string): string {
4140
</script>
4241

4342
<template>
44-
<div :class="depth === 0 ? 'flex gap-3' : 'flex gap-3 mt-3'">
43+
<div class="flex gap-3">
4544
<!-- Avatar -->
4645
<a
4746
:href="`https://bsky.app/profile/${comment.author.handle}`"
@@ -53,7 +52,7 @@ function getHostname(uri: string): string {
5352
v-if="comment.author.avatar"
5453
:src="comment.author.avatar"
5554
:alt="comment.author.displayName || comment.author.handle"
56-
:class="['rounded-full', depth === 0 ? 'w-10 h-10' : 'w-8 h-8']"
55+
:class="['rounded-full', isReply ? 'w-8 h-8' : 'w-10 h-10']"
5756
width="40"
5857
height="40"
5958
loading="lazy"
@@ -62,14 +61,19 @@ function getHostname(uri: string): string {
6261
v-else
6362
:class="[
6463
'rounded-full bg-border flex items-center justify-center text-fg-muted',
65-
depth === 0 ? 'w-10 h-10' : 'w-8 h-8 text-sm',
64+
isReply ? 'w-8 h-8 text-sm' : 'w-10 h-10',
6665
]"
6766
>
6867
{{ (comment.author.displayName || comment.author.handle).charAt(0).toUpperCase() }}
6968
</div>
7069
</a>
7170

7271
<div class="flex-1 min-w-0">
72+
<!-- Replying to label -->
73+
<div v-if="replyingTo" class="text-xs text-fg-subtle mb-0.5">
74+
{{ $t('blog.atproto.replying_to', { name: replyingTo }) }}
75+
</div>
76+
7377
<!-- Author info + timestamp -->
7478
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-0">
7579
<a
@@ -171,33 +175,6 @@ function getHostname(uri: string): string {
171175
{{ $t('blog.atproto.repost_count', { count: comment.repostCount }, comment.repostCount) }}
172176
</span>
173177
</div>
174-
175-
<!-- Nested replies -->
176-
<template v-if="comment.replies.length > 0">
177-
<div v-if="depth < MaxDepth" class="mt-2 ps-2 border-is-2 border-border flex flex-col">
178-
<BlueskyComment
179-
v-for="reply in comment.replies"
180-
:key="reply.uri"
181-
:comment="reply"
182-
:depth="depth + 1"
183-
/>
184-
</div>
185-
<a
186-
v-else
187-
:href="getCommentUrl(comment.replies[0]!)"
188-
target="_blank"
189-
rel="noopener noreferrer"
190-
class="mt-2 block text-sm link"
191-
>
192-
{{
193-
$t(
194-
'blog.atproto.more_replies',
195-
{ count: comment.replies.length },
196-
comment.replies.length,
197-
)
198-
}}
199-
</a>
200-
</template>
201178
</div>
202179
</div>
203180
</template>
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<script setup lang="ts">
2+
import type { Comment } from '#shared/types/blog-post'
3+
4+
const props = defineProps<{
5+
comment: Comment
6+
}>()
7+
8+
interface FlatReply {
9+
comment: Comment
10+
replyingTo?: string
11+
}
12+
13+
function flattenReplies(comment: Comment): FlatReply[] {
14+
const result: FlatReply[] = []
15+
function walk(replies: Comment[], parentName: string, isDirectReply: boolean) {
16+
for (const reply of replies) {
17+
result.push({
18+
comment: reply,
19+
replyingTo: isDirectReply ? undefined : parentName,
20+
})
21+
if (reply.replies.length > 0) {
22+
walk(reply.replies, reply.author.displayName || reply.author.handle, false)
23+
}
24+
}
25+
}
26+
walk(comment.replies, comment.author.displayName || comment.author.handle, true)
27+
return result
28+
}
29+
30+
const flatReplies = computed(() => flattenReplies(props.comment))
31+
const totalReplyCount = computed(() => flatReplies.value.length)
32+
const showReplies = ref(false)
33+
</script>
34+
35+
<template>
36+
<div>
37+
<!-- Top-level comment -->
38+
<BlueskyComment :comment="comment" />
39+
40+
<!-- Replies section -->
41+
<div v-if="totalReplyCount > 0" class="ms-13 mt-2">
42+
<!-- Toggle button -->
43+
<button
44+
v-if="!showReplies"
45+
class="text-sm text-accent font-medium hover:underline cursor-pointer"
46+
@click="showReplies = true"
47+
>
48+
{{ $t('blog.atproto.view_replies', { count: totalReplyCount }, totalReplyCount) }}
49+
</button>
50+
51+
<!-- Expanded replies -->
52+
<template v-else>
53+
<button
54+
class="text-sm text-accent font-medium hover:underline mb-3 cursor-pointer"
55+
@click="showReplies = false"
56+
>
57+
{{ $t('blog.atproto.hide_replies') }}
58+
</button>
59+
60+
<div class="flex flex-col gap-4">
61+
<BlueskyComment
62+
v-for="reply in flatReplies"
63+
:key="reply.comment.uri"
64+
:comment="reply.comment"
65+
:replying-to="reply.replyingTo"
66+
is-reply
67+
/>
68+
</div>
69+
</template>
70+
</div>
71+
</div>
72+
</template>

app/components/BlueskyComments.vue

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,7 @@ const postUrl = computed(() => data.value?.postUrl)
100100
</LinkBase>
101101
</div>
102102

103-
<BlueskyComment
104-
v-for="reply in thread.replies"
105-
:key="reply.uri"
106-
:comment="reply"
107-
:depth="0"
108-
/>
103+
<BlueskyCommentThread v-for="reply in thread.replies" :key="reply.uri" :comment="reply" />
109104

110105
<LinkBase v-if="postUrl" variant="button-primary" :to="postUrl">
111106
{{ $t('blog.atproto.like_or_reply_on_bluesky') }}

app/components/Link/Base.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ const keyboardShortcutsEnabled = useKeyboardShortcuts()
101101
'text-xs px-2 py-0.5': isButtonSmall,
102102
'bg-transparent text-fg hover:(bg-fg/10 text-accent) focus-visible:(bg-fg/10 text-accent) aria-[current=true]:(bg-fg/10 text-accent border-fg/20 hover:enabled:(bg-fg/20 text-fg/50))':
103103
variant === 'button-secondary',
104-
'text-bg bg-fg hover:(bg-fg/50 text-accent) focus-visible:(bg-fg/50) aria-current:(bg-fg text-bg border-fg hover:enabled:(text-bg/50))':
104+
'text-bg bg-fg hover:(bg-fg/80 text-bg) focus-visible:(bg-fg/80 text-bg) aria-current:(bg-fg text-bg border-fg hover:enabled:(text-bg/50))':
105105
variant === 'button-primary',
106106
}"
107107
:to="to"

app/components/global/BlogPostFederatedArticles.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const federatedArticles = computed(() => {
4141
</script>
4242

4343
<template>
44-
<aside class="px-4 sm:-mx-6 sm:px-6 sm:-my-3 sm:py-3 sm:rounded-md">
44+
<aside class="sm:-mx-6 sm:px-6 sm:-my-3 sm:py-3 sm:rounded-md">
4545
<h2 class="font-mono text-xl font-medium text-fg mt-0">
4646
{{ headline }}
4747
</h2>
@@ -55,7 +55,7 @@ const federatedArticles = computed(() => {
5555
rel="noopener noreferrer"
5656
v-for="article in federatedArticles"
5757
:key="article.url"
58-
class="grid grid-cols-[auto_1fr] gap-x-5 no-underline hover:no-underline rounded-lg border border-border p-4 transition-shadow hover:shadow-lg hover:shadow-gray-500/50"
58+
class="grid grid-cols-[auto_1fr] gap-x-5 no-underline hover:no-underline rounded-lg border border-border p-4 transition-all hover:shadow-md hover:shadow-black/5 dark:hover:shadow-white/5 hover:border-border-hover"
5959
>
6060
<AuthorAvatar v-if="article.author" :author="article.author" size="md" class="row-span-2" />
6161
<div class="flex flex-col">

app/components/global/BlogPostWrapper.vue

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ const blueskyPostUri = computed(() => blueskyLink.value?.postUri ?? null)
5858
<AuthorList :authors="post.authors" variant="expanded" />
5959
</div>
6060
</div>
61-
<article class="max-w-prose mx-auto p-2 prose dark:prose-invert">
61+
<article class="max-w-prose mx-auto prose dark:prose-invert">
6262
<div class="text-sm text-fg-muted font-mono mb-4">
6363
<DateTime :datetime="frontmatter.date" year="numeric" month="short" day="numeric" />
6464
</div>
@@ -77,4 +77,19 @@ const blueskyPostUri = computed(() => blueskyLink.value?.postUri ?? null)
7777
:deep(.markdown-body) {
7878
@apply prose dark:prose-invert;
7979
}
80+
81+
:deep(.prose a:not(.not-prose a)) {
82+
text-decoration: underline;
83+
text-underline-offset: 0.2rem;
84+
text-decoration-thickness: 1px;
85+
text-decoration-color: var(--fg-subtle);
86+
transition:
87+
text-decoration-color 0.2s,
88+
color 0.2s;
89+
}
90+
91+
:deep(.prose a:not(.not-prose a):hover) {
92+
text-decoration-color: var(--fg);
93+
color: var(--fg);
94+
}
8095
</style>

app/components/global/BlueskyPostEmbed.client.vue

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,16 @@ const postUrl = computed(() => {
107107
:href="postUrl ?? '#'"
108108
target="_blank"
109109
rel="noopener noreferrer"
110-
class="not-prose block rounded-lg border border-border bg-bg-subtle p-4 sm:p-5 no-underline hover:border-border-hover transition-colors duration-200"
110+
class="not-prose block rounded-lg border border-border bg-bg-subtle p-4 sm:p-5 no-underline hover:border-border-hover transition-colors duration-200 relative"
111111
>
112+
<!-- Bluesky icon -->
113+
<span
114+
class="i-simple-icons:bluesky w-5 h-5 text-fg-subtle absolute top-4 end-4 sm:top-5 sm:end-5"
115+
aria-hidden="true"
116+
/>
117+
112118
<!-- Author row -->
113-
<div class="flex items-center gap-3 mb-3">
119+
<div class="flex items-center gap-3 mb-3 pe-7">
114120
<img
115121
v-if="post.author.avatar"
116122
:src="`${post.author.avatar}?size=48`"
@@ -126,10 +132,6 @@ const postUrl = computed(() => {
126132
</div>
127133
<div class="text-sm text-fg-subtle truncate">@{{ post.author.handle }}</div>
128134
</div>
129-
<span
130-
class="i-simple-icons:bluesky w-5 h-5 text-fg-subtle ms-auto shrink-0"
131-
aria-hidden="true"
132-
/>
133135
</div>
134136

135137
<!-- Post text -->

0 commit comments

Comments
 (0)