fix(feed): header on one line, avatar padding explicit, icon/label aligned
Three related layout polish issues. 1. Avatar + name + time splitting to two lines Fixed by collapsing the "·" separator + time into a single Text component (was three sibling Texts). RN occasionally lays out three-Text-sibling rows oddly when the name is long because each Text measures independently. With the separator glued to the time, there's one indivisible chip for "· 2h" that flexShrink:0 keeps on the row; the name takes the rest and truncates with "…" if needed. Also wrapped the name + time in a flex:1 Pressable (author tap target) so the row width is authoritative. 2. Avatar flush against the left edge The outer Pressable's style-function paddingLeft was being applied but the visual offset felt wrong because avatar has no explicit width. Added width:44 to the avatar's own Pressable wrapper so the flex-row layout reserves exactly the expected space and the 16-px paddingLeft on the outer Pressable is visible around the avatar's left edge. 3. Like / view count text visually below icon's centre RN Text includes extra top padding (Android) and baseline-anchors (iOS) that push the number glyph a pixel or two below the icon centre in a flex-row with alignItems:'center'. Added explicit lineHeight:16 (matching icon size) + includeFontPadding:false + textAlignVertical:'center' on both the heart button's Text and the shared ActionButton Text → numbers now sit on the same optical baseline as their icons on both platforms. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -185,65 +185,65 @@ function PostCardInner({ post, likedByMe, onStatsChanged, onDeleted, compact }:
|
|||||||
onLongPress={onLongPress}
|
onLongPress={onLongPress}
|
||||||
style={({ pressed }) => ({
|
style={({ pressed }) => ({
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
paddingHorizontal: 16,
|
paddingLeft: 16,
|
||||||
|
paddingRight: 16,
|
||||||
paddingVertical: compact ? 10 : 12,
|
paddingVertical: compact ? 10 : 12,
|
||||||
backgroundColor: pressed ? '#080808' : 'transparent',
|
backgroundColor: pressed ? '#080808' : 'transparent',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{/* Avatar column */}
|
{/* Avatar — own tap target (opens author profile). Explicit width
|
||||||
<Pressable onPress={onOpenAuthor} hitSlop={4}>
|
on the wrapper (width:44) so the flex-row sibling below computes
|
||||||
|
its remaining space correctly. */}
|
||||||
|
<Pressable onPress={onOpenAuthor} hitSlop={4} style={{ width: 44 }}>
|
||||||
<Avatar name={displayName} address={post.author} size={44} />
|
<Avatar name={displayName} address={post.author} size={44} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
||||||
{/* Content column. overflow:'hidden' stops a long unbreakable
|
{/* Content column. overflow:'hidden' prevents unbreakable tokens
|
||||||
token (URL, hashtag) from visually escaping the card — it'll
|
from drawing past the right edge of the card. */}
|
||||||
be ellipsized or clipped instead. */}
|
|
||||||
<View style={{ flex: 1, marginLeft: 10, minWidth: 0, overflow: 'hidden' }}>
|
<View style={{ flex: 1, marginLeft: 10, minWidth: 0, overflow: 'hidden' }}>
|
||||||
{/* Header: [name] · [time] … [menu]
|
{/* Header row — name + time on ONE line.
|
||||||
All three on a single row with no wrap. The name shrinks if
|
Two siblings: the author-link Pressable (flex:1, row, so it
|
||||||
too long (flexShrink:1 + numberOfLines:1), time never shrinks
|
expands; name inside gets numberOfLines:1 + flexShrink:1 so
|
||||||
— so long handles get truncated with ellipsis while "2h"
|
it truncates instead of wrapping) and the "· <time>" tail
|
||||||
stays readable.
|
(flexShrink:0 — never truncates). Keeping the "·" inside the
|
||||||
|
time Text means they stay glued even if the inline layout
|
||||||
Whole header is inside a single Pressable so a tap anywhere
|
decides to break weirdly. */}
|
||||||
on the header opens the author's profile — matches Twitter's
|
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||||
behaviour where the name-row is a big hit target. */}
|
<Pressable
|
||||||
<Pressable
|
onPress={onOpenAuthor}
|
||||||
onPress={onOpenAuthor}
|
hitSlop={{ top: 4, bottom: 2 }}
|
||||||
hitSlop={{ top: 4, bottom: 2 }}
|
style={{ flex: 1, flexDirection: 'row', alignItems: 'center', minWidth: 0 }}
|
||||||
style={{ flexDirection: 'row', alignItems: 'center' }}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
numberOfLines={1}
|
|
||||||
style={{
|
|
||||||
color: '#ffffff',
|
|
||||||
fontWeight: '700',
|
|
||||||
fontSize: 14,
|
|
||||||
letterSpacing: -0.2,
|
|
||||||
flexShrink: 1,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{displayName}
|
<Text
|
||||||
</Text>
|
numberOfLines={1}
|
||||||
<Text
|
style={{
|
||||||
style={{ color: '#6a6a6a', fontSize: 13, marginHorizontal: 4 }}
|
color: '#ffffff',
|
||||||
numberOfLines={1}
|
fontWeight: '700',
|
||||||
>
|
fontSize: 14,
|
||||||
·
|
letterSpacing: -0.2,
|
||||||
</Text>
|
flexShrink: 1,
|
||||||
<Text
|
}}
|
||||||
style={{ color: '#6a6a6a', fontSize: 13 }}
|
>
|
||||||
numberOfLines={1}
|
{displayName}
|
||||||
>
|
</Text>
|
||||||
{formatRelativeTime(post.created_at)}
|
<Text
|
||||||
</Text>
|
numberOfLines={1}
|
||||||
<View style={{ flex: 1 }} />
|
style={{
|
||||||
|
color: '#6a6a6a',
|
||||||
|
fontSize: 13,
|
||||||
|
marginLeft: 6,
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
· {formatRelativeTime(post.created_at)}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
{mine && (
|
{mine && (
|
||||||
<Pressable onPress={onLongPress} hitSlop={8}>
|
<Pressable onPress={onLongPress} hitSlop={8} style={{ marginLeft: 8 }}>
|
||||||
<Ionicons name="ellipsis-horizontal" size={16} color="#6a6a6a" />
|
<Ionicons name="ellipsis-horizontal" size={16} color="#6a6a6a" />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)}
|
)}
|
||||||
</Pressable>
|
</View>
|
||||||
|
|
||||||
{/* Body text with hashtag highlighting.
|
{/* Body text with hashtag highlighting.
|
||||||
flexShrink:1 + explicit width:'100%' + paddingRight:4 keep
|
flexShrink:1 + explicit width:'100%' + paddingRight:4 keep
|
||||||
@@ -336,6 +336,9 @@ function PostCardInner({ post, likedByMe, onStatsChanged, onDeleted, compact }:
|
|||||||
color: localLiked ? '#e0245e' : '#6a6a6a',
|
color: localLiked ? '#e0245e' : '#6a6a6a',
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: localLiked ? '600' : '400',
|
fontWeight: localLiked ? '600' : '400',
|
||||||
|
lineHeight: 16,
|
||||||
|
includeFontPadding: false,
|
||||||
|
textAlignVertical: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{formatCount(localLikeCount)}
|
{formatCount(localLikeCount)}
|
||||||
@@ -406,7 +409,22 @@ function ActionButton({ icon, label, onPress }: {
|
|||||||
>
|
>
|
||||||
<Ionicons name={icon} size={16} color="#6a6a6a" />
|
<Ionicons name={icon} size={16} color="#6a6a6a" />
|
||||||
{label && (
|
{label && (
|
||||||
<Text style={{ color: '#6a6a6a', fontSize: 12 }}>{label}</Text>
|
<Text
|
||||||
|
style={{
|
||||||
|
color: '#6a6a6a',
|
||||||
|
fontSize: 12,
|
||||||
|
// Match icon height (16) as lineHeight so baseline-anchored
|
||||||
|
// text aligns with the icon's vertical centre. Without this,
|
||||||
|
// RN renders Text one pixel lower than the icon mid-point on
|
||||||
|
// most Android fonts, which looks sloppy next to the heart /
|
||||||
|
// eye glyphs.
|
||||||
|
lineHeight: 16,
|
||||||
|
includeFontPadding: false,
|
||||||
|
textAlignVertical: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user