Fix full-width message layouts (#591)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
thomaspblock
2026-05-17 09:04:09 -04:00
committed by GitHub
co-authored by Cursor
parent e4e9923799
commit 17eea2d267
13 changed files with 187 additions and 182 deletions
@@ -401,7 +401,7 @@ export const ChannelPane = React.memo(function ChannelPane({
showTopBorder={false}
/>
<div className="h-7 bg-background px-4 pb-1 pt-0 sm:px-6 -mt-1">
<div className="mx-auto flex h-full w-full max-w-4xl items-center gap-2">
<div className="flex h-full w-full items-center gap-2">
{hasComposerBotActivity ? (
<div className="shrink-0">
<BotActivityComposerAction
@@ -277,7 +277,7 @@ export function InboxDetailPane({
<div className="px-6 py-2">
<article
className={cn(
"group/message flex items-start gap-2.5 px-2 py-1 transition-colors duration-1000",
"group/message relative flex items-start gap-2.5 px-2 py-1 transition-colors duration-1000",
message.isSelected
? cn(
isFocusHighlightVisible
@@ -292,6 +292,39 @@ export function InboxDetailPane({
: "home-inbox-context-message"
}
>
{canReply || onToggleReaction ? (
<div className="absolute right-2 top-1 z-10">
<MessageActionBar
activeReplyTargetId={replyTargetId}
message={toActionBarMessage(message)}
onReactionSelect={
onToggleReaction
? (emoji) => {
const actionBarMessage =
toActionBarMessage(message);
const remove =
actionBarMessage.reactions?.some(
(reaction) =>
reaction.emoji === emoji &&
reaction.reactedByCurrentUser,
) ?? false;
return onToggleReaction(
actionBarMessage,
emoji,
remove,
);
}
: undefined
}
onReply={
canReply
? () => handleSelectReplyTarget(message)
: undefined
}
reactions={message.reactions ?? []}
/>
</div>
) : null}
<UserAvatar
avatarUrl={message.avatarUrl}
className="h-8 w-8 shrink-0 rounded-xl"
@@ -299,53 +332,18 @@ export function InboxDetailPane({
size="md"
/>
<div className="-mt-1 min-w-0 flex-1">
<div className="flex min-w-0 flex-wrap items-start gap-x-2 gap-y-0">
<div className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0">
<p className="truncate text-sm font-semibold leading-none tracking-tight text-foreground">
{message.authorLabel}
</p>
<p className="shrink-0 text-xs font-normal leading-none tabular-nums text-muted-foreground/55">
{message.fullTimestampLabel}
</p>
{message.isSelected ? (
<span className="text-[10px] font-semibold uppercase leading-none tracking-[0.14em] text-muted-foreground/70">
Inbox item
</span>
) : null}
<p className="ml-auto text-xs text-muted-foreground">
{message.fullTimestampLabel}
</p>
{canReply || onToggleReaction ? (
<div className="relative ml-1">
<div className="absolute right-0 top-1/2 -translate-y-1/2">
<MessageActionBar
activeReplyTargetId={replyTargetId}
message={toActionBarMessage(message)}
onReactionSelect={
onToggleReaction
? (emoji) => {
const actionBarMessage =
toActionBarMessage(message);
const remove =
actionBarMessage.reactions?.some(
(reaction) =>
reaction.emoji === emoji &&
reaction.reactedByCurrentUser,
) ?? false;
return onToggleReaction(
actionBarMessage,
emoji,
remove,
);
}
: undefined
}
onReply={
canReply
? () => handleSelectReplyTarget(message)
: undefined
}
reactions={message.reactions ?? []}
/>
</div>
</div>
) : null}
</div>
<div className="mt-1">
<Markdown
@@ -2,7 +2,7 @@ export function DayDivider({ label }: { label: string }) {
return (
<section
aria-label={label}
className="sticky top-11 z-20 flex justify-center py-1"
className="sticky top-11 z-[5] flex justify-center py-1"
data-testid="message-timeline-day-divider"
data-day-label={label}
>
@@ -559,7 +559,7 @@ export function MessageComposer({
aria-hidden="true"
className="absolute inset-x-0 bottom-0 h-5 bg-background"
/>
<div className="relative mx-auto flex w-full max-w-4xl flex-col gap-3">
<div className="relative flex w-full flex-col gap-3">
<form
className="relative isolate rounded-2xl border border-border/50 bg-background/70 px-3 pb-2 pt-3 shadow-[0_4px_24px_rgba(0,0,0,0.08)] backdrop-blur-xl supports-[backdrop-filter]:bg-background/55 dark:shadow-[0_4px_24px_rgba(0,0,0,0.35)] sm:px-4"
data-testid="message-composer"
+39 -36
View File
@@ -178,26 +178,28 @@ export const MessageRow = React.memo(
</h3>
);
const metadataNode = (
<div className="ml-auto flex items-center gap-2 text-xs text-muted-foreground">
<div className="relative">
<div className="absolute right-0 top-1/2 -translate-y-1/2">
<MessageActionBar
activeReplyTargetId={activeReplyTargetId}
message={message}
onDelete={onDelete}
onEdit={onEdit}
onMarkUnread={onMarkUnread}
onReactionSelect={
canToggleReactions ? handleReactionSelect : undefined
}
onReply={onReply}
reactionErrorMessage={reactionErrorMessage}
reactionPending={reactionPending}
reactions={reactions}
/>
</div>
</div>
const actionBarNode = (
<div className="absolute right-2 top-1 z-10">
<MessageActionBar
activeReplyTargetId={activeReplyTargetId}
message={message}
onDelete={onDelete}
onEdit={onEdit}
onMarkUnread={onMarkUnread}
onReactionSelect={
canToggleReactions ? handleReactionSelect : undefined
}
onReply={onReply}
reactionErrorMessage={reactionErrorMessage}
reactionPending={reactionPending}
reactions={reactions}
/>
</div>
);
const inlineMetadataNode = (
<div className="flex shrink-0 items-baseline gap-2 text-xs">
<MessageTimestamp createdAt={message.createdAt} time={message.time} />
{message.pending ? (
<p className="font-medium uppercase tracking-[0.14em] text-primary/80">
Sending
@@ -211,7 +213,6 @@ export const MessageRow = React.memo(
<TooltipContent>This message has been edited</TooltipContent>
</Tooltip>
) : null}
<MessageTimestamp createdAt={message.createdAt} time={message.time} />
</div>
);
@@ -281,7 +282,7 @@ export const MessageRow = React.memo(
<article
className={cn(
"group/message rounded-2xl px-2 py-1 transition-colors",
"group/message relative rounded-2xl px-2 py-1 transition-colors",
isThreadReplyLayout ? "space-y-1" : "flex items-start gap-2.5",
highlighted ? "bg-primary/10 ring-1 ring-primary/30" : "",
)}
@@ -290,7 +291,7 @@ export const MessageRow = React.memo(
>
{isThreadReplyLayout ? (
<>
<div className="flex min-w-0 items-start gap-1.5">
<div className="flex min-w-0 items-baseline gap-1.5">
{message.pubkey ? (
<UserProfilePopover
pubkey={message.pubkey}
@@ -298,7 +299,10 @@ export const MessageRow = React.memo(
botIdenticonValue={message.author}
>
<button
className="flex shrink-0 items-start gap-1.5 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
className={cn(
"flex shrink-0 items-start gap-1.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
avatarButtonRadiusClass,
)}
type="button"
>
{avatarNode}
@@ -313,16 +317,14 @@ export const MessageRow = React.memo(
{authorNode}
</>
)}
<div className="min-w-0 flex-1">
<div className="flex min-w-0 flex-wrap items-start gap-x-2 gap-y-0.5">
{message.personaDisplayName &&
message.personaDisplayName !== message.author ? (
<span className="text-xs text-muted-foreground">
{message.personaDisplayName}
</span>
) : null}
{metadataNode}
</div>
<div className="flex min-w-0 flex-1 flex-wrap items-baseline gap-x-2 gap-y-0.5">
{inlineMetadataNode}
{message.personaDisplayName &&
message.personaDisplayName !== message.author ? (
<span className="text-xs text-muted-foreground">
{message.personaDisplayName}
</span>
) : null}
</div>
</div>
<div className="min-w-0 space-y-0.5">{messageBodyNode}</div>
@@ -349,7 +351,7 @@ export const MessageRow = React.memo(
<div className="flex shrink-0 items-start">{avatarNode}</div>
)}
<div className="-mt-1 min-w-0 flex-1 space-y-0">
<div className="flex min-w-0 flex-wrap items-start gap-x-2 gap-y-0">
<div className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0">
{message.pubkey ? (
<UserProfilePopover
pubkey={message.pubkey}
@@ -366,18 +368,19 @@ export const MessageRow = React.memo(
) : (
authorNode
)}
{inlineMetadataNode}
{message.personaDisplayName &&
message.personaDisplayName !== message.author ? (
<span className="text-xs text-muted-foreground">
{message.personaDisplayName}
</span>
) : null}
{metadataNode}
</div>
<div className="-mt-0.5">{messageBodyNode}</div>
</div>
</>
)}
{actionBarNode}
</article>
</div>
);
@@ -135,7 +135,7 @@ export const MessageTimeline = React.memo(function MessageTimeline({
ref={scrollContainerRef}
>
<div
className="mx-auto flex w-full max-w-4xl flex-col gap-2 pb-10 pt-16"
className="flex w-full flex-col gap-2 pb-10 pt-16"
ref={contentRef}
>
<div ref={topSentinelRef} aria-hidden className="h-px" />
@@ -11,7 +11,9 @@ export function MessageTimestamp({
return (
<Tooltip>
<TooltipTrigger asChild>
<p className="cursor-default whitespace-nowrap">{time}</p>
<p className="shrink-0 cursor-default whitespace-nowrap text-xs font-normal leading-none tabular-nums text-muted-foreground/55">
{time}
</p>
</TooltipTrigger>
<TooltipContent side="top">
{formatFullDateTime(createdAt)}
@@ -200,7 +200,7 @@ export const SystemMessageRow = React.memo(function SystemMessageRow({
return (
<div
className="group/message rounded-2xl px-2 py-1 transition-colors"
className="group/message relative rounded-2xl px-2 py-1 transition-colors"
data-testid="system-message-row"
>
<div className="flex items-start gap-2.5">
@@ -211,9 +211,15 @@ export const SystemMessageRow = React.memo(function SystemMessageRow({
testId="system-message-avatar"
/>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-semibold leading-none tracking-tight text-foreground/90">
{description.title}
</p>
<div className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5">
<p className="truncate text-sm font-semibold leading-none tracking-tight text-foreground/90">
{description.title}
</p>
<MessageTimestamp
createdAt={message.createdAt}
time={message.time}
/>
</div>
<p className="mt-1 text-sm leading-snug text-muted-foreground/70">
{description.action}
</p>
@@ -235,86 +241,77 @@ export const SystemMessageRow = React.memo(function SystemMessageRow({
) : null}
</div>
</div>
<div className="ml-auto flex items-center gap-1 text-xs text-muted-foreground/60">
<div className="relative">
<div className="absolute right-0 top-1/2 -translate-y-1/2">
{canToggleReactions ? (
<div
className={cn(
"overflow-hidden rounded-full border border-border/70 bg-background/95 shadow-sm backdrop-blur supports-[backdrop-filter]:bg-background/85 transition-all duration-150 ease-out",
"max-w-0 border-0 shadow-none translate-y-1 opacity-0",
"group-hover/message:max-w-9 group-hover/message:border group-hover/message:border-border/70 group-hover/message:shadow-sm group-hover/message:translate-y-0 group-hover/message:opacity-100",
"group-focus-within/message:max-w-9 group-focus-within/message:border group-focus-within/message:border-border/70 group-focus-within/message:shadow-sm group-focus-within/message:translate-y-0 group-focus-within/message:opacity-100",
isReactionPickerOpen
? "max-w-9 border border-border/70 shadow-sm translate-y-0 opacity-100"
: "",
)}
<div className="absolute right-2 top-1 z-10">
{canToggleReactions ? (
<div
className={cn(
"overflow-hidden rounded-full border border-border/70 bg-background/95 shadow-sm backdrop-blur supports-[backdrop-filter]:bg-background/85 transition-all duration-150 ease-out",
"max-w-0 border-0 shadow-none translate-y-1 opacity-0",
"group-hover/message:max-w-9 group-hover/message:border group-hover/message:border-border/70 group-hover/message:shadow-sm group-hover/message:translate-y-0 group-hover/message:opacity-100",
"group-focus-within/message:max-w-9 group-focus-within/message:border group-focus-within/message:border-border/70 group-focus-within/message:shadow-sm group-focus-within/message:translate-y-0 group-focus-within/message:opacity-100",
isReactionPickerOpen
? "max-w-9 border border-border/70 shadow-sm translate-y-0 opacity-100"
: "",
)}
>
<div className="flex items-center gap-1 p-1">
<Popover
onOpenChange={setIsReactionPickerOpen}
open={isReactionPickerOpen}
>
<div className="flex items-center gap-1 p-1">
<Popover
onOpenChange={setIsReactionPickerOpen}
open={isReactionPickerOpen}
>
<Tooltip>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<Button
aria-label="Open reactions"
className="h-6 w-6 rounded-full p-0"
disabled={reactionPending}
size="sm"
type="button"
variant={
isReactionPickerOpen ? "secondary" : "ghost"
}
>
{reactionPending ? (
<Spinner className="h-3 w-3" />
) : (
<SmilePlus className="h-3 w-3" />
)}
</Button>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent>React</TooltipContent>
</Tooltip>
<PopoverContent
align="end"
className="w-auto p-0 rounded-2xl overflow-hidden border-0 bg-transparent shadow-none"
side="top"
sideOffset={10}
>
{reactionErrorMessage ? (
<div className="px-3 pt-3 pb-0">
<p className="text-xs text-destructive">
{reactionErrorMessage}
</p>
</div>
) : null}
<Picker
data={data}
onEmojiSelect={(emoji: { native: string }) => {
void handleReactionSelect(emoji.native).finally(
() => {
setIsReactionPickerOpen(false);
},
);
}}
theme="auto"
previewPosition="none"
skinTonePosition="search"
set="native"
maxFrequentRows={2}
perLine={8}
/>
</PopoverContent>
</Popover>
</div>
</div>
) : null}
<Tooltip>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<Button
aria-label="Open reactions"
className="h-6 w-6 rounded-full p-0"
disabled={reactionPending}
size="sm"
type="button"
variant={isReactionPickerOpen ? "secondary" : "ghost"}
>
{reactionPending ? (
<Spinner className="h-3 w-3" />
) : (
<SmilePlus className="h-3 w-3" />
)}
</Button>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent>React</TooltipContent>
</Tooltip>
<PopoverContent
align="end"
className="w-auto p-0 rounded-2xl overflow-hidden border-0 bg-transparent shadow-none"
side="top"
sideOffset={10}
>
{reactionErrorMessage ? (
<div className="px-3 pt-3 pb-0">
<p className="text-xs text-destructive">
{reactionErrorMessage}
</p>
</div>
) : null}
<Picker
data={data}
onEmojiSelect={(emoji: { native: string }) => {
void handleReactionSelect(emoji.native).finally(() => {
setIsReactionPickerOpen(false);
});
}}
theme="auto"
previewPosition="none"
skinTonePosition="search"
set="native"
maxFrequentRows={2}
perLine={8}
/>
</PopoverContent>
</Popover>
</div>
</div>
</div>
<MessageTimestamp createdAt={message.createdAt} time={message.time} />
) : null}
</div>
</div>
</div>
@@ -5,13 +5,16 @@ export function TimelineSkeleton() {
return (
<>
{skeletonRows.map((row) => (
{skeletonRows.map((row, index) => (
<div className="flex gap-2.5" key={row}>
<Skeleton className="h-8 w-8 rounded-lg" />
<Skeleton className="h-9 w-9 shrink-0 rounded-xl" />
<div className="min-w-0 flex-1 space-y-1">
<Skeleton className="h-3.5 w-44" />
<Skeleton className="h-4 w-full max-w-2xl" />
<Skeleton className="h-4 w-full max-w-xl" />
<div className="flex items-baseline gap-2">
<Skeleton className="h-3.5 w-28" />
<Skeleton className="h-3 w-12" />
</div>
<Skeleton className="h-4 w-full" />
<Skeleton className={index % 2 === 0 ? "h-4 w-4/5" : "h-4 w-2/3"} />
</div>
</div>
))}
@@ -91,7 +91,6 @@ export function TypingIndicatorRow({
className={cn(
"flex w-full items-center gap-2",
isActivityVariant && "h-full",
!isActivityVariant && "mx-auto max-w-4xl",
)}
>
<div className="flex shrink-0 items-center">
+11 -8
View File
@@ -45,13 +45,16 @@ function LoadingHeaderSkeleton() {
function MessageRowsSkeleton() {
return (
<>
{["first", "second", "third", "fourth", "fifth"].map((row) => (
{["first", "second", "third", "fourth", "fifth"].map((row, index) => (
<div className="flex gap-3" key={row}>
<Skeleton className="h-8 w-8 shrink-0 rounded-lg" />
<div className="min-w-0 flex-1 space-y-2 pt-0.5">
<Skeleton className="h-3.5 w-40 max-w-[40%]" />
<Skeleton className="h-4 w-full max-w-3xl" />
<Skeleton className="h-4 w-full max-w-xl" />
<Skeleton className="h-9 w-9 shrink-0 rounded-xl" />
<div className="min-w-0 flex-1 space-y-1 pt-0.5">
<div className="flex items-baseline gap-2">
<Skeleton className="h-3.5 w-28" />
<Skeleton className="h-3 w-12" />
</div>
<Skeleton className="h-4 w-full" />
<Skeleton className={index % 2 === 0 ? "h-4 w-4/5" : "h-4 w-2/3"} />
</div>
</div>
))}
@@ -177,13 +180,13 @@ function ChannelLoadingBody() {
return (
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
<div className="flex-1 overflow-y-auto px-4 py-3 sm:px-6">
<div className="mx-auto flex w-full max-w-4xl flex-col gap-4">
<div className="flex w-full flex-col gap-4">
<MessageRowsSkeleton />
</div>
</div>
<div className="border-t border-border/60 bg-background px-4 py-4 sm:px-6">
<div className="mx-auto w-full max-w-4xl space-y-3">
<div className="w-full space-y-3">
<Skeleton className="h-10 w-full rounded-2xl" />
<div className="flex items-center gap-2">
<Skeleton className="h-8 w-20 rounded-lg" />
+3 -3
View File
@@ -60,7 +60,7 @@ function CopyableUrl({ url }: { url: string }) {
function DetailSkeleton() {
return (
<div className="mx-auto flex w-full max-w-7xl gap-8 px-4 py-8">
<div className="flex w-full gap-8 px-4 py-8">
<div className="min-w-0 flex-1">
<div className="h-5 w-24 animate-pulse rounded bg-muted" />
<div className="mt-6 h-8 w-64 animate-pulse rounded bg-muted" />
@@ -181,7 +181,7 @@ export function RepoDetailPage() {
if (!repo) {
return (
<div className="mx-auto flex w-full max-w-7xl gap-8 px-4 py-8">
<div className="flex w-full gap-8 px-4 py-8">
<div className="min-w-0 flex-1">
<Link
to="/"
@@ -205,7 +205,7 @@ export function RepoDetailPage() {
}
return (
<div className="mx-auto flex w-full max-w-7xl gap-8 px-4 py-8">
<div className="flex w-full gap-8 px-4 py-8">
{/* Main content */}
<div className="min-w-0 flex-1">
{/* Back link */}
+2 -2
View File
@@ -101,7 +101,7 @@ export function ReposPage() {
if (isLoading) {
return (
<div className="mx-auto flex w-full max-w-7xl gap-8 px-4 py-8">
<div className="flex w-full gap-8 px-4 py-8">
<div className="min-w-0 flex-1">
<h2 className="mb-4 flex items-center gap-2 text-lg font-semibold">
<BookMarked className="h-5 w-5" /> Repositories
@@ -122,7 +122,7 @@ export function ReposPage() {
}
return (
<div className="mx-auto flex w-full max-w-7xl gap-8 px-4 py-8">
<div className="flex w-full gap-8 px-4 py-8">
{/* Main content */}
<div className="min-w-0 flex-1">
{/* Mobile-only connect button */}