Polish thread and media layout (#1239)

This commit is contained in:
klopez4212
2026-06-24 12:55:25 -07:00
committed by GitHub
parent 092546495d
commit b264a32cfb
12 changed files with 173 additions and 140 deletions
@@ -22,7 +22,7 @@ export function HomeLoadingState() {
key={row}
>
<div className="relative shrink-0">
<Skeleton className="h-8 w-8 rounded-full" />
<Skeleton className="h-9 w-9 rounded-full" />
{row === "first" || row === "third" ? (
<Skeleton className="absolute -right-1 -top-1 h-2.5 w-2.5 rounded-full" />
) : null}
@@ -71,7 +71,7 @@ export function HomeLoadingState() {
<div className="mx-1 mb-3 border-t border-border/60" />
) : null}
<article className="relative flex items-start gap-2.5">
<Skeleton className="h-8 w-8 shrink-0 rounded-full" />
<Skeleton className="h-9 w-9 shrink-0 rounded-full" />
<div className="min-w-0 flex-1">
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-0">
<Skeleton className="h-4 w-24" />
@@ -169,7 +169,7 @@ export function InboxListPane({
>
<button
className={cn(
"relative block w-full border-l px-3 py-4 text-left transition-colors after:pointer-events-none after:absolute after:bottom-0 after:left-[3.375rem] after:right-0 after:h-px after:bg-border/45 after:content-['']",
"relative block w-full border-l px-3 py-4 text-left transition-colors after:pointer-events-none after:absolute after:bottom-0 after:left-[3.625rem] after:right-0 after:h-px after:bg-border/45 after:content-['']",
isSelected
? "border-l-transparent bg-[var(--inbox-row-highlight-bg)]"
: "border-l-transparent group-hover/inbox-item:bg-[var(--inbox-row-highlight-bg)] group-focus-within/inbox-item:bg-[var(--inbox-row-highlight-bg)] active:bg-muted/40",
@@ -182,7 +182,7 @@ export function InboxListPane({
<div className="relative shrink-0">
<UserAvatar
avatarUrl={item.avatarUrl}
className="h-8 w-8"
className="h-9 w-9"
displayName={item.senderLabel}
size="md"
/>
@@ -116,7 +116,7 @@ export function InboxMessageRow({
<div className="relative shrink-0">
<UserAvatar
avatarUrl={message.avatarUrl}
className="h-8 w-8 shrink-0"
className="h-9 w-9 shrink-0"
displayName={message.authorLabel}
size="md"
/>
@@ -2,48 +2,55 @@ import assert from "node:assert/strict";
import test from "node:test";
import {
getThreadReplyAvatarCenterPx,
getThreadReplyAvatarCenterYPx,
getThreadReplyAvatarCenterRem,
getThreadReplyAvatarCenterYRem,
getThreadReplyConnectorLayout,
getThreadReplyDescendantRailStartYPx,
getThreadReplyIndentPx,
getThreadReplyDescendantRailStartYRem,
getThreadReplyIndentRem,
threadReplyLength,
} from "./threadTreeLayout.ts";
test("getThreadReplyIndentPx aligns child avatars to parent text columns", () => {
assert.equal(getThreadReplyIndentPx(0), 0);
assert.equal(getThreadReplyIndentPx(1), 50);
assert.equal(getThreadReplyIndentPx(2), 100);
assert.equal(getThreadReplyIndentPx(3), 150);
test("getThreadReplyIndentRem uses a visible Tailwind spacing step", () => {
assert.equal(getThreadReplyIndentRem(0), 0);
assert.equal(getThreadReplyIndentRem(1), 2.25);
assert.equal(getThreadReplyIndentRem(2), 4.5);
assert.equal(getThreadReplyIndentRem(3), 6.75);
});
test("avatar center helpers expose the rail anchor points", () => {
assert.equal(getThreadReplyAvatarCenterPx(0), 32);
assert.equal(getThreadReplyAvatarCenterPx(1), 82);
assert.equal(getThreadReplyAvatarCenterYPx(), 28);
assert.equal(getThreadReplyDescendantRailStartYPx(), 52);
assert.equal(getThreadReplyAvatarCenterRem(0), 1.875);
assert.equal(getThreadReplyAvatarCenterRem(1), 4.125);
assert.equal(getThreadReplyAvatarCenterYRem(), 1.5);
assert.equal(getThreadReplyDescendantRailStartYRem(), 2.875);
});
test("getThreadReplyConnectorLayout stops before the child avatar edge", () => {
assert.equal(getThreadReplyConnectorLayout(0), null);
assert.deepEqual(getThreadReplyConnectorLayout(1), {
childOffsetPx: 82,
heightPx: 28,
parentOffsetPx: 32,
widthPx: 26,
childOffsetRem: 4.125,
heightRem: 1.5,
parentOffsetRem: 1.875,
widthRem: 0.875,
});
assert.deepEqual(getThreadReplyConnectorLayout(2), {
childOffsetPx: 132,
heightPx: 28,
parentOffsetPx: 82,
widthPx: 26,
childOffsetRem: 6.375,
heightRem: 1.5,
parentOffsetRem: 4.125,
widthRem: 0.875,
});
});
test("getThreadReplyConnectorLayout clamps very deep replies to the visible rail", () => {
assert.deepEqual(getThreadReplyConnectorLayout(99), {
childOffsetPx: 332,
heightPx: 28,
parentOffsetPx: 282,
widthPx: 26,
childOffsetRem: 15.375,
heightRem: 1.5,
parentOffsetRem: 13.125,
widthRem: 0.875,
});
});
test("threadReplyLength formats rem values for inline styles", () => {
assert.equal(threadReplyLength(0), "0");
assert.equal(threadReplyLength(1.75), "1.75rem");
assert.equal(threadReplyLength(-0.125), "-0.125rem");
});
@@ -1,51 +1,60 @@
const THREAD_REPLY_MAX_VISIBLE_DEPTH = 6;
const THREAD_REPLY_AVATAR_SIZE_PX = 40;
const THREAD_REPLY_ROW_CONTENT_INSET_PX = 12;
const THREAD_REPLY_ROW_CONTENT_GAP_PX = 10;
const THREAD_REPLY_ROW_PADDING_TOP_PX = 8;
const THREAD_REPLY_AVATAR_RADIUS_PX = THREAD_REPLY_AVATAR_SIZE_PX / 2;
const THREAD_REPLY_AVATAR_LINE_GAP_PX = 4;
const THREAD_REPLY_AVATAR_SIZE_REM = 2.25; // Tailwind size-9
const THREAD_REPLY_ROW_MARGIN_INLINE_REM = 0.25; // Tailwind mx-1
const THREAD_REPLY_ROW_CONTENT_INSET_REM = 0.5; // Tailwind px-2
const THREAD_REPLY_ROW_CONTENT_GAP_REM = 0.625; // Tailwind gap-2.5
const THREAD_REPLY_ROW_PADDING_TOP_REM = 0.375; // Tailwind py-1.5
const THREAD_REPLY_DEPTH_STEP_REM = 2.25; // Tailwind spacing-9
const THREAD_REPLY_AVATAR_RADIUS_REM = THREAD_REPLY_AVATAR_SIZE_REM / 2;
const THREAD_REPLY_AVATAR_LINE_GAP_REM = 0.25; // Tailwind spacing-1
export const THREAD_REPLY_BODY_OFFSET_PX =
THREAD_REPLY_ROW_CONTENT_INSET_PX +
THREAD_REPLY_AVATAR_SIZE_PX +
THREAD_REPLY_ROW_CONTENT_GAP_PX;
export const THREAD_REPLY_ROOT_INDENT_PX =
THREAD_REPLY_BODY_OFFSET_PX - THREAD_REPLY_ROW_CONTENT_INSET_PX;
export const THREAD_REPLY_NESTED_INDENT_PX = THREAD_REPLY_ROOT_INDENT_PX;
export const THREAD_REPLY_LINE_WIDTH_PX = 1.5;
export const THREAD_REPLY_BODY_OFFSET_REM =
THREAD_REPLY_ROW_MARGIN_INLINE_REM +
THREAD_REPLY_ROW_CONTENT_INSET_REM +
THREAD_REPLY_AVATAR_SIZE_REM +
THREAD_REPLY_ROW_CONTENT_GAP_REM;
export const THREAD_REPLY_ROOT_INDENT_REM = THREAD_REPLY_DEPTH_STEP_REM;
export const THREAD_REPLY_NESTED_INDENT_REM = THREAD_REPLY_ROOT_INDENT_REM;
export const THREAD_REPLY_LINE_WIDTH_REM = 0.09375;
const THREAD_REPLY_AVATAR_CENTER_OFFSET_PX =
THREAD_REPLY_ROW_CONTENT_INSET_PX + THREAD_REPLY_AVATAR_SIZE_PX / 2;
const THREAD_REPLY_AVATAR_CENTER_Y_PX =
THREAD_REPLY_ROW_PADDING_TOP_PX + THREAD_REPLY_AVATAR_SIZE_PX / 2;
const THREAD_REPLY_AVATAR_CENTER_OFFSET_REM =
THREAD_REPLY_ROW_MARGIN_INLINE_REM +
THREAD_REPLY_ROW_CONTENT_INSET_REM +
THREAD_REPLY_AVATAR_SIZE_REM / 2;
const THREAD_REPLY_AVATAR_CENTER_Y_REM =
THREAD_REPLY_ROW_PADDING_TOP_REM + THREAD_REPLY_AVATAR_SIZE_REM / 2;
export function threadReplyLength(valueRem: number) {
if (valueRem === 0) return "0";
return `${Number(valueRem.toFixed(5))}rem`;
}
function clampVisibleDepth(depth: number) {
return Math.min(Math.max(depth, 0), THREAD_REPLY_MAX_VISIBLE_DEPTH);
}
export function getThreadReplyIndentPx(depth: number) {
export function getThreadReplyIndentRem(depth: number) {
const visibleDepth = clampVisibleDepth(depth);
return visibleDepth > 0
? THREAD_REPLY_ROOT_INDENT_PX +
(visibleDepth - 1) * THREAD_REPLY_NESTED_INDENT_PX
? THREAD_REPLY_ROOT_INDENT_REM +
(visibleDepth - 1) * THREAD_REPLY_NESTED_INDENT_REM
: 0;
}
export function getThreadReplyAvatarCenterPx(depth: number) {
return getThreadReplyIndentPx(depth) + THREAD_REPLY_AVATAR_CENTER_OFFSET_PX;
export function getThreadReplyAvatarCenterRem(depth: number) {
return getThreadReplyIndentRem(depth) + THREAD_REPLY_AVATAR_CENTER_OFFSET_REM;
}
export function getThreadReplyAvatarCenterYPx() {
return THREAD_REPLY_AVATAR_CENTER_Y_PX;
export function getThreadReplyAvatarCenterYRem() {
return THREAD_REPLY_AVATAR_CENTER_Y_REM;
}
export function getThreadReplyDescendantRailStartYPx() {
export function getThreadReplyDescendantRailStartYRem() {
return (
THREAD_REPLY_AVATAR_CENTER_Y_PX +
THREAD_REPLY_AVATAR_RADIUS_PX +
THREAD_REPLY_AVATAR_LINE_GAP_PX
THREAD_REPLY_AVATAR_CENTER_Y_REM +
THREAD_REPLY_AVATAR_RADIUS_REM +
THREAD_REPLY_AVATAR_LINE_GAP_REM
);
}
@@ -55,17 +64,17 @@ export function getThreadReplyConnectorLayout(depth: number) {
return null;
}
const parentOffsetPx = getThreadReplyAvatarCenterPx(visibleDepth - 1);
const childOffsetPx = getThreadReplyAvatarCenterPx(visibleDepth);
const childEdgeOffsetPx =
childOffsetPx -
THREAD_REPLY_AVATAR_RADIUS_PX -
THREAD_REPLY_AVATAR_LINE_GAP_PX;
const parentOffsetRem = getThreadReplyAvatarCenterRem(visibleDepth - 1);
const childOffsetRem = getThreadReplyAvatarCenterRem(visibleDepth);
const childEdgeOffsetRem =
childOffsetRem -
THREAD_REPLY_AVATAR_RADIUS_REM -
THREAD_REPLY_AVATAR_LINE_GAP_REM;
return {
childOffsetPx,
heightPx: THREAD_REPLY_AVATAR_CENTER_Y_PX,
parentOffsetPx,
widthPx: Math.max(0, childEdgeOffsetPx - parentOffsetPx),
childOffsetRem,
heightRem: THREAD_REPLY_AVATAR_CENTER_Y_REM,
parentOffsetRem,
widthRem: Math.max(0, childEdgeOffsetRem - parentOffsetRem),
};
}
+47 -37
View File
@@ -7,12 +7,13 @@ import type { UserProfileLookup } from "@/features/profile/lib/identity";
import { UserProfilePopover } from "@/features/profile/ui/UserProfilePopover";
import { useRemindLater } from "@/features/reminders/ui/RemindMeLaterProvider";
import {
getThreadReplyAvatarCenterPx,
getThreadReplyAvatarCenterYPx,
getThreadReplyDescendantRailStartYPx,
getThreadReplyAvatarCenterRem,
getThreadReplyAvatarCenterYRem,
getThreadReplyDescendantRailStartYRem,
getThreadReplyConnectorLayout,
getThreadReplyIndentPx,
THREAD_REPLY_LINE_WIDTH_PX,
getThreadReplyIndentRem,
threadReplyLength,
THREAD_REPLY_LINE_WIDTH_REM,
} from "@/features/messages/lib/threadTreeLayout";
import { KIND_STREAM_MESSAGE_DIFF } from "@/shared/constants/kinds";
import { cn } from "@/shared/lib/cn";
@@ -186,9 +187,9 @@ export const MessageRow = React.memo(
[channels],
);
const indentPx = getThreadReplyIndentPx(message.depth);
const descendantGuideOffsetPx = connectDescendants
? getThreadReplyAvatarCenterPx(message.depth)
const indentRem = getThreadReplyIndentRem(message.depth);
const descendantGuideOffsetRem = connectDescendants
? getThreadReplyAvatarCenterRem(message.depth)
: null;
const replyConnector = React.useMemo(() => {
return getThreadReplyConnectorLayout(message.depth);
@@ -200,7 +201,7 @@ export const MessageRow = React.memo(
return depths.map((depth) => ({
depth,
offset: getThreadReplyAvatarCenterPx(depth),
offset: getThreadReplyAvatarCenterRem(depth),
}));
}, [depthGuideDepths, message.depth]);
const handleCollapseDescendants = React.useCallback(
@@ -293,8 +294,7 @@ export const MessageRow = React.memo(
};
const isThreadReplyLayout = layoutVariant === "thread-reply";
const guideBleedPx = isThreadReplyLayout ? 4 : 0;
const avatarSizeClass = "!h-10 !w-10";
const guideBleedRem = isThreadReplyLayout ? 0.25 : 0;
const avatarButtonRadiusClass = "rounded-full";
const respondToDotColor =
@@ -309,7 +309,7 @@ export const MessageRow = React.memo(
<UserAvatar
accent={message.accent}
avatarUrl={message.avatarUrl ?? null}
className={cn("shrink-0", avatarSizeClass)}
className="shrink-0"
displayName={message.author}
testId="message-avatar"
/>
@@ -442,7 +442,11 @@ export const MessageRow = React.memo(
return (
<div
className="relative"
style={indentPx > 0 ? { paddingLeft: `${indentPx}px` } : undefined}
style={
indentRem > 0
? { paddingLeft: threadReplyLength(indentRem) }
: undefined
}
>
{showDepthGuides && depthGuideItems.length > 0 ? (
<div
@@ -455,8 +459,8 @@ export const MessageRow = React.memo(
"pointer-events-none",
)}
style={{
bottom: `${-guideBleedPx}px`,
top: `${-guideBleedPx}px`,
bottom: threadReplyLength(-guideBleedRem),
top: threadReplyLength(-guideBleedRem),
}}
>
{depthGuideItems.map(({ depth, offset }) => {
@@ -472,11 +476,13 @@ export const MessageRow = React.memo(
aria-hidden
className={cn(
"pointer-events-none absolute bottom-0 top-0 border-l transition-[border-color]",
isHighlighted ? "border-primary" : "border-border",
isHighlighted ? "border-primary" : "border-border/45",
)}
style={{
borderLeftWidth: `${THREAD_REPLY_LINE_WIDTH_PX}px`,
left: `${offset}px`,
borderLeftWidth: threadReplyLength(
THREAD_REPLY_LINE_WIDTH_REM,
),
left: threadReplyLength(offset),
}}
/>
<button
@@ -511,7 +517,7 @@ export const MessageRow = React.memo(
false,
)
}
style={{ left: `${offset}px` }}
style={{ left: threadReplyLength(offset) }}
type="button"
/>
</React.Fragment>
@@ -523,31 +529,33 @@ export const MessageRow = React.memo(
aria-hidden
className={cn(
"pointer-events-none absolute bottom-0 top-0 border-l transition-[border-color]",
isHighlighted ? "border-primary" : "border-border",
isHighlighted ? "border-primary" : "border-border/45",
)}
key={`${message.id}-depth-guide-${offset}`}
style={{
borderLeftWidth: `${THREAD_REPLY_LINE_WIDTH_PX}px`,
left: `${offset}px`,
borderLeftWidth: threadReplyLength(
THREAD_REPLY_LINE_WIDTH_REM,
),
left: threadReplyLength(offset),
}}
/>
);
})}
</div>
) : null}
{showDepthGuides && descendantGuideOffsetPx !== null ? (
{showDepthGuides && descendantGuideOffsetRem !== null ? (
<>
<div
aria-hidden
className={cn(
"pointer-events-none absolute bottom-0 z-0 border-l transition-[border-color]",
highlightDescendantRail ? "border-primary" : "border-border",
highlightDescendantRail ? "border-primary" : "border-border/45",
)}
style={{
bottom: `${-guideBleedPx}px`,
borderLeftWidth: `${THREAD_REPLY_LINE_WIDTH_PX}px`,
left: `${descendantGuideOffsetPx}px`,
top: `${getThreadReplyDescendantRailStartYPx()}px`,
bottom: threadReplyLength(-guideBleedRem),
borderLeftWidth: threadReplyLength(THREAD_REPLY_LINE_WIDTH_REM),
left: threadReplyLength(descendantGuideOffsetRem),
top: threadReplyLength(getThreadReplyDescendantRailStartYRem()),
}}
/>
{onCollapseDescendants ? (
@@ -564,8 +572,8 @@ export const MessageRow = React.memo(
onMouseEnter={() => handleCollapseDescendantsHoverChange(true)}
onMouseLeave={() => handleCollapseDescendantsHoverChange(false)}
style={{
left: `${descendantGuideOffsetPx}px`,
top: `${getThreadReplyAvatarCenterYPx()}px`,
left: threadReplyLength(descendantGuideOffsetRem),
top: threadReplyLength(getThreadReplyAvatarCenterYRem()),
}}
type="button"
/>
@@ -577,15 +585,17 @@ export const MessageRow = React.memo(
aria-hidden
className={cn(
"pointer-events-none absolute left-0 top-0 rounded-bl-2xl border-b border-l transition-[border-color]",
highlightReplyConnector ? "border-primary" : "border-border",
highlightReplyConnector ? "border-primary" : "border-border/45",
)}
style={{
borderBottomWidth: `${THREAD_REPLY_LINE_WIDTH_PX}px`,
borderLeftWidth: `${THREAD_REPLY_LINE_WIDTH_PX}px`,
height: `${replyConnector.heightPx + guideBleedPx}px`,
left: `${replyConnector.parentOffsetPx}px`,
top: `${-guideBleedPx}px`,
width: `${replyConnector.widthPx}px`,
borderBottomWidth: threadReplyLength(THREAD_REPLY_LINE_WIDTH_REM),
borderLeftWidth: threadReplyLength(THREAD_REPLY_LINE_WIDTH_REM),
height: threadReplyLength(
replyConnector.heightRem + guideBleedRem,
),
left: threadReplyLength(replyConnector.parentOffsetRem),
top: threadReplyLength(-guideBleedRem),
width: threadReplyLength(replyConnector.widthRem),
}}
/>
) : null}
@@ -94,12 +94,10 @@ type MessageThreadPanelProps = {
onUnfollowThread?: () => void;
};
/** Stable `useDeferredValue` initial value; mirrors `EMPTY_MESSAGES`. */
const EMPTY_THREAD_REPLIES: MainTimelineEntry[] = [];
const THREAD_PANEL_MESSAGE_GUTTER_CLASS = "px-2";
const THREAD_PANEL_COMPOSER_GUTTER_CLASS = "px-5";
const THREAD_PANEL_SUMMARY_INDENT_OFFSET_PX = -2;
const THREAD_PANEL_SUMMARY_INDENT_OFFSET_REM = -0.125;
type MessageThreadPanelSkeletonProps = {
isSinglePanelView?: boolean;
layout?: "standalone" | "split";
@@ -696,7 +694,9 @@ export function MessageThreadPanel({
message={threadHead}
onOpenThread={expandThreadHeadReplies}
summary={visibleThreadHeadSummary}
summaryIndentOffsetPx={THREAD_PANEL_SUMMARY_INDENT_OFFSET_PX}
summaryIndentOffsetRem={
THREAD_PANEL_SUMMARY_INDENT_OFFSET_REM
}
unreadCount={threadUnreadCount}
/>
</div>
@@ -820,8 +820,8 @@ export function MessageThreadPanel({
}
onOpenThread={onExpandReplies}
summary={entry.summary}
summaryIndentOffsetPx={
THREAD_PANEL_SUMMARY_INDENT_OFFSET_PX
summaryIndentOffsetRem={
THREAD_PANEL_SUMMARY_INDENT_OFFSET_REM
}
showDepthGuides={shouldShowThreadBranchGuides}
unreadCount={threadReplyUnreadCounts?.get(
@@ -8,10 +8,11 @@ import type { TimelineMessage } from "@/features/messages/types";
import type { ThreadDepthGuideAction } from "@/features/messages/ui/MessageRow";
import { formatThreadSummaryLastReplyTime } from "@/features/messages/lib/dateFormatters";
import {
getThreadReplyAvatarCenterPx,
getThreadReplyIndentPx,
THREAD_REPLY_BODY_OFFSET_PX,
THREAD_REPLY_LINE_WIDTH_PX,
getThreadReplyAvatarCenterRem,
getThreadReplyIndentRem,
threadReplyLength,
THREAD_REPLY_BODY_OFFSET_REM,
THREAD_REPLY_LINE_WIDTH_REM,
} from "@/features/messages/lib/threadTreeLayout";
import { cn } from "@/shared/lib/cn";
import { UserAvatar } from "@/shared/ui/UserAvatar";
@@ -59,7 +60,7 @@ export function MessageThreadSummaryRow({
onOpenThread,
showDepthGuides = true,
summary,
summaryIndentOffsetPx = 0,
summaryIndentOffsetRem = 0,
unreadCount,
}: {
collapseDepthGuideActions?: ReadonlyArray<ThreadDepthGuideAction>;
@@ -75,12 +76,12 @@ export function MessageThreadSummaryRow({
onOpenThread: (message: TimelineMessage) => void;
showDepthGuides?: boolean;
summary: TimelineThreadSummary;
summaryIndentOffsetPx?: number;
summaryIndentOffsetRem?: number;
unreadCount?: number;
}) {
const indentPx = getThreadReplyIndentPx(depth);
const marginLeftPx =
indentPx + THREAD_REPLY_BODY_OFFSET_PX + summaryIndentOffsetPx;
const indentRem = getThreadReplyIndentRem(depth);
const marginLeftRem =
indentRem + THREAD_REPLY_BODY_OFFSET_REM + summaryIndentOffsetRem;
const replyLabel = summary.replyCount === 1 ? "reply" : "replies";
const summaryAriaLabel = summary.lastReplyAt
? `View thread with ${summary.replyCount} ${replyLabel}, last reply ${formatThreadSummaryLastReplyTime(summary.lastReplyAt)}`
@@ -90,7 +91,7 @@ export function MessageThreadSummaryRow({
: Array.from({ length: depth }, (_, index) => index);
const depthGuideItems = guideDepths.map((guideDepth) => ({
depth: guideDepth,
offset: getThreadReplyAvatarCenterPx(guideDepth),
offset: getThreadReplyAvatarCenterRem(guideDepth),
}));
const collapseDepthGuideActionsByDepth = new Map(
collapseDepthGuideActions?.map((action) => [action.depth, action]) ?? [],
@@ -108,7 +109,7 @@ export function MessageThreadSummaryRow({
collapseDepthGuideActionsByDepth.size === 0 &&
"pointer-events-none",
)}
style={{ bottom: "-4px", top: "-4px" }}
style={{ bottom: "-0.25rem", top: "-0.25rem" }}
>
{depthGuideItems.map(({ depth: guideDepth, offset }) => {
const collapseAction =
@@ -125,11 +126,13 @@ export function MessageThreadSummaryRow({
aria-hidden
className={cn(
"pointer-events-none absolute bottom-0 top-0 border-l transition-[border-color]",
isHighlighted ? "border-primary" : "border-border",
isHighlighted ? "border-primary" : "border-border/45",
)}
style={{
borderLeftWidth: `${THREAD_REPLY_LINE_WIDTH_PX}px`,
left: `${offset}px`,
borderLeftWidth: threadReplyLength(
THREAD_REPLY_LINE_WIDTH_REM,
),
left: threadReplyLength(offset),
}}
/>
<button
@@ -166,7 +169,7 @@ export function MessageThreadSummaryRow({
false,
)
}
style={{ left: `${offset}px` }}
style={{ left: threadReplyLength(offset) }}
type="button"
/>
</React.Fragment>
@@ -178,12 +181,14 @@ export function MessageThreadSummaryRow({
aria-hidden
className={cn(
"pointer-events-none absolute bottom-0 top-0 border-l transition-[border-color]",
isHighlighted ? "border-primary" : "border-border",
isHighlighted ? "border-primary" : "border-border/45",
)}
key={`${message.id}-summary-depth-guide-${offset}`}
style={{
borderLeftWidth: `${THREAD_REPLY_LINE_WIDTH_PX}px`,
left: `${offset}px`,
borderLeftWidth: threadReplyLength(
THREAD_REPLY_LINE_WIDTH_REM,
),
left: threadReplyLength(offset),
}}
/>
);
@@ -197,7 +202,7 @@ export function MessageThreadSummaryRow({
data-thread-head-id={message.id}
data-testid="message-thread-summary"
onClick={() => onOpenThread(message)}
style={{ marginLeft: `${marginLeftPx}px` }}
style={{ marginLeft: threadReplyLength(marginLeftRem) }}
type="button"
>
<div className="ml-0.5 flex shrink-0 items-center">
+2 -1
View File
@@ -1443,7 +1443,8 @@
}
.buzz-spoiler--block img[data-spoiler-media-size] {
height: var(--buzz-spoiler-media-height);
aspect-ratio: var(--buzz-spoiler-media-aspect-ratio);
height: auto;
width: var(--buzz-spoiler-media-width);
}
+1 -1
View File
@@ -11,7 +11,7 @@ type UserAvatarSize = "xs" | "sm" | "md";
const sizeClasses: Record<UserAvatarSize, string> = {
xs: "h-5 w-5 text-3xs",
sm: "h-6 w-6 text-2xs",
md: "h-10 w-10 text-xs",
md: "h-9 w-9 text-xs",
};
type UserAvatarProps = {
+1 -1
View File
@@ -10,7 +10,7 @@ const Avatar = React.forwardRef<
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
"relative flex h-9 w-9 shrink-0 overflow-hidden rounded-full",
className,
)}
{...props}
+7 -6
View File
@@ -1118,9 +1118,10 @@ function ImageBlock({
const spoilerMediaStyle = hiddenSpoilerMediaSize
? ({
"--buzz-spoiler-media-height": `${hiddenSpoilerMediaSize.height}px`,
"--buzz-spoiler-media-aspect-ratio": `${hiddenSpoilerMediaSize.width} / ${hiddenSpoilerMediaSize.height}`,
"--buzz-spoiler-media-width": `${hiddenSpoilerMediaSize.width}px`,
height: `${hiddenSpoilerMediaSize.height}px`,
aspectRatio: `${hiddenSpoilerMediaSize.width} / ${hiddenSpoilerMediaSize.height}`,
height: "auto",
width: `${hiddenSpoilerMediaSize.width}px`,
} as React.CSSProperties)
: undefined;
@@ -1202,7 +1203,7 @@ function ImageBlock({
aria-hidden={isHiddenInSpoiler ? true : undefined}
aria-label={alt?.trim() ? `Zoom image: ${alt}` : "Zoom image"}
className={cn(
"mt-1 inline-block max-w-full cursor-zoom-in rounded-xl border-0 bg-transparent p-0 text-left align-top focus:outline-hidden focus-visible:ring-2 focus-visible:ring-ring/50",
"mt-1 inline-block min-w-0 max-w-full cursor-zoom-in rounded-xl border-0 bg-transparent p-0 text-left align-top focus:outline-hidden focus-visible:ring-2 focus-visible:ring-ring/50",
lightboxBox && "opacity-0",
)}
data-testid="message-image-lightbox-trigger"
@@ -1214,7 +1215,7 @@ function ImageBlock({
>
<img
alt={alt}
className="block max-h-64 max-w-sm rounded-xl object-contain"
className="block h-auto max-h-64 max-w-[min(24rem,100%)] rounded-xl object-contain"
data-spoiler-media-size={hiddenSpoilerMediaSize ? "" : undefined}
height={intrinsicDimensions?.height}
ref={imageRef}
@@ -1981,7 +1982,7 @@ function createMarkdownComponents(
}
const entry = src ? imetaByUrl?.get(src) : undefined;
return (
<span data-block-media="" className="block">
<span data-block-media="" className="block min-w-0 max-w-full">
<ImageBlock
alt={alt}
dim={entry?.dim}
@@ -2005,7 +2006,7 @@ function createMarkdownComponents(
if (isImageOnlyParagraph(childArray)) {
return (
<div className="mt-1 grid max-w-lg grid-cols-2 gap-1.5 [&_br]:hidden [&_[data-block-media]]:mt-0 [&_[data-block-media]]:max-w-none [&_img]:mt-0 [&_img]:w-full [&_img]:max-w-full">
<div className="mt-1 grid w-full min-w-0 max-w-lg grid-cols-2 gap-1.5 [&_br]:hidden [&_[data-block-media]]:mt-0 [&_[data-block-media]]:max-w-none [&_img]:mt-0 [&_img]:w-full [&_img]:max-w-full">
{imageChildren}
</div>
);