mirror of
https://github.com/block/buzz.git
synced 2026-10-31 22:41:12 +00:00
fix(desktop): polish timeline spacing and panel chrome
Align sticky day headers, thread summaries, and composer offsets so message content stays visible and date transitions happen at the right moment. Soften side-sheet headers and markdown code blocks while keeping thread conversations auto-scrolled as new content arrives. Made-with: Cursor
This commit is contained in:
@@ -43,12 +43,12 @@ export function PersonaCatalogDetailsSheet({
|
||||
return (
|
||||
<Sheet onOpenChange={onOpenChange} open={open}>
|
||||
<SheetContent
|
||||
className="w-full overflow-y-auto sm:max-w-xl"
|
||||
className="flex w-full flex-col gap-0 overflow-hidden bg-background p-0 sm:max-w-xl"
|
||||
data-testid="persona-catalog-details-sheet"
|
||||
>
|
||||
{persona ? (
|
||||
<div className="space-y-6 pr-4">
|
||||
<SheetHeader className="border-b border-border/60 pb-4 pr-10">
|
||||
<>
|
||||
<SheetHeader className="relative z-10 space-y-4 bg-background/25 px-6 py-6 pr-16 text-left shadow-[0_4px_24px_rgba(0,0,0,0.06)] backdrop-blur-xl supports-[backdrop-filter]:bg-background/20 dark:shadow-[0_4px_24px_rgba(0,0,0,0.25)]">
|
||||
<div className="flex items-start gap-3">
|
||||
<ProfileAvatar
|
||||
avatarUrl={persona.avatarUrl}
|
||||
@@ -68,93 +68,94 @@ export function PersonaCatalogDetailsSheet({
|
||||
</div>
|
||||
</div>
|
||||
</SheetHeader>
|
||||
<div className="flex-1 space-y-6 overflow-y-auto px-6 py-6">
|
||||
<button
|
||||
aria-label={getPersonaCatalogSelectionAriaLabel(
|
||||
persona.displayName,
|
||||
isSelected,
|
||||
)}
|
||||
aria-pressed={isSelected}
|
||||
className={cn(
|
||||
"w-full rounded-xl border p-4 text-left transition-[background-color,border-color,box-shadow] focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-offset-2",
|
||||
isSelected
|
||||
? "border-primary bg-primary/10 text-foreground"
|
||||
: "border-border/80 bg-background/60 text-muted-foreground hover:bg-accent hover:text-accent-foreground",
|
||||
isPending && "cursor-not-allowed opacity-70",
|
||||
)}
|
||||
data-state={isSelected ? "selected" : "available"}
|
||||
data-testid={`persona-catalog-detail-selection-target-${persona.id}`}
|
||||
disabled={isPending}
|
||||
onClick={() => {
|
||||
onTogglePersona(persona);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p
|
||||
className="text-sm font-semibold tracking-tight"
|
||||
data-testid="persona-catalog-detail-selection-title"
|
||||
>
|
||||
{selectionCopy.title}
|
||||
</p>
|
||||
<p
|
||||
className="mt-1 text-sm text-muted-foreground"
|
||||
data-testid="persona-catalog-detail-selection-description"
|
||||
>
|
||||
{selectionCopy.description}
|
||||
</p>
|
||||
</div>
|
||||
<PersonaCatalogSelectionBadge isActive={isSelected} />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
aria-label={getPersonaCatalogSelectionAriaLabel(
|
||||
persona.displayName,
|
||||
isSelected,
|
||||
)}
|
||||
aria-pressed={isSelected}
|
||||
className={cn(
|
||||
"w-full rounded-xl border p-4 text-left transition-[background-color,border-color,box-shadow] focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-offset-2",
|
||||
isSelected
|
||||
? "border-primary bg-primary/10 text-foreground"
|
||||
: "border-border/80 bg-background/60 text-muted-foreground hover:bg-accent hover:text-accent-foreground",
|
||||
isPending && "cursor-not-allowed opacity-70",
|
||||
)}
|
||||
data-state={isSelected ? "selected" : "available"}
|
||||
data-testid={`persona-catalog-detail-selection-target-${persona.id}`}
|
||||
disabled={isPending}
|
||||
onClick={() => {
|
||||
onTogglePersona(persona);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p
|
||||
className="text-sm font-semibold tracking-tight"
|
||||
data-testid="persona-catalog-detail-selection-title"
|
||||
>
|
||||
{selectionCopy.title}
|
||||
{feedbackNoticeMessage ? (
|
||||
<p className="rounded-2xl border border-primary/20 bg-primary/10 px-4 py-3 text-sm text-primary">
|
||||
{feedbackNoticeMessage}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{feedbackErrorMessage ? (
|
||||
<p className="rounded-2xl border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
||||
{feedbackErrorMessage}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div className="rounded-xl border border-border/70 bg-card/70 p-4">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">
|
||||
Type
|
||||
</p>
|
||||
<p
|
||||
className="mt-1 text-sm text-muted-foreground"
|
||||
data-testid="persona-catalog-detail-selection-description"
|
||||
>
|
||||
{selectionCopy.description}
|
||||
<p className="mt-2 text-sm font-medium">Built-in persona</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-border/70 bg-card/70 p-4">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">
|
||||
Preferred model
|
||||
</p>
|
||||
<p className="mt-2 text-sm font-medium">
|
||||
{persona.model ?? "Use app default"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-border/70 bg-card/70 p-4 sm:col-span-2">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">
|
||||
Preferred provider
|
||||
</p>
|
||||
<p className="mt-2 text-sm font-medium">
|
||||
{persona.provider ?? "Use app default"}
|
||||
</p>
|
||||
</div>
|
||||
<PersonaCatalogSelectionBadge isActive={isSelected} />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{feedbackNoticeMessage ? (
|
||||
<p className="rounded-2xl border border-primary/20 bg-primary/10 px-4 py-3 text-sm text-primary">
|
||||
{feedbackNoticeMessage}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{feedbackErrorMessage ? (
|
||||
<p className="rounded-2xl border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
||||
{feedbackErrorMessage}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div className="rounded-xl border border-border/70 bg-card/70 p-4">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">
|
||||
Type
|
||||
</p>
|
||||
<p className="mt-2 text-sm font-medium">Built-in persona</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-border/70 bg-card/70 p-4">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">
|
||||
Preferred model
|
||||
</p>
|
||||
<p className="mt-2 text-sm font-medium">
|
||||
{persona.model ?? "Use app default"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-border/70 bg-card/70 p-4 sm:col-span-2">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">
|
||||
Preferred provider
|
||||
</p>
|
||||
<p className="mt-2 text-sm font-medium">
|
||||
{persona.provider ?? "Use app default"}
|
||||
System prompt
|
||||
</p>
|
||||
<pre className="mt-3 whitespace-pre-wrap break-words font-sans text-sm leading-6 text-foreground">
|
||||
{persona.systemPrompt}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-border/70 bg-card/70 p-4">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">
|
||||
System prompt
|
||||
</p>
|
||||
<pre className="mt-3 whitespace-pre-wrap break-words font-sans text-sm leading-6 text-foreground">
|
||||
{persona.systemPrompt}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
@@ -38,7 +38,6 @@ import {
|
||||
} from "@/shared/ui/alert-dialog";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Separator } from "@/shared/ui/separator";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
@@ -214,7 +213,7 @@ export function ChannelManagementSheet({
|
||||
data-testid="channel-management-sheet"
|
||||
side="right"
|
||||
>
|
||||
<SheetHeader className="space-y-4 border-b border-border/80 bg-muted/20 px-6 py-6 text-left">
|
||||
<SheetHeader className="relative z-10 space-y-4 bg-background/25 px-6 py-6 text-left shadow-[0_4px_24px_rgba(0,0,0,0.06)] backdrop-blur-xl supports-[backdrop-filter]:bg-background/20 dark:shadow-[0_4px_24px_rgba(0,0,0,0.25)]">
|
||||
<div className="space-y-2">
|
||||
<SheetTitle className="pr-8">{channel.name}</SheetTitle>
|
||||
<SheetDescription>
|
||||
@@ -314,8 +313,6 @@ export function ChannelManagementSheet({
|
||||
</p>
|
||||
) : null}
|
||||
</Section>
|
||||
|
||||
<Separator />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
@@ -330,8 +327,6 @@ export function ChannelManagementSheet({
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Separator />
|
||||
|
||||
<Section
|
||||
description="Topic and purpose show the current context for the channel."
|
||||
title="Context"
|
||||
@@ -415,8 +410,6 @@ export function ChannelManagementSheet({
|
||||
</form>
|
||||
</Section>
|
||||
|
||||
<Separator />
|
||||
|
||||
<Section
|
||||
description="Name and description are owner/admin actions."
|
||||
title="Details"
|
||||
@@ -481,8 +474,6 @@ export function ChannelManagementSheet({
|
||||
|
||||
{resolvedChannel.channelType !== "dm" ? (
|
||||
<>
|
||||
<Separator />
|
||||
|
||||
<Section
|
||||
description="Archiving keeps history but blocks new changes."
|
||||
title="Channel state"
|
||||
@@ -541,8 +532,6 @@ export function ChannelManagementSheet({
|
||||
|
||||
{isOwner && resolvedChannel.channelType !== "dm" ? (
|
||||
<>
|
||||
<Separator />
|
||||
|
||||
<Section
|
||||
description="Deleting removes the channel from the workspace list."
|
||||
title="Danger zone"
|
||||
|
||||
@@ -183,7 +183,7 @@ export function MembersSidebar({
|
||||
data-testid="members-sidebar"
|
||||
side="right"
|
||||
>
|
||||
<SheetHeader className="space-y-2 border-b border-border/80 bg-muted/20 px-6 py-6 text-left">
|
||||
<SheetHeader className="relative z-10 space-y-2 bg-background/25 px-6 py-6 text-left shadow-[0_4px_24px_rgba(0,0,0,0.06)] backdrop-blur-xl supports-[backdrop-filter]:bg-background/20 dark:shadow-[0_4px_24px_rgba(0,0,0,0.25)]">
|
||||
<SheetTitle>Members</SheetTitle>
|
||||
<SheetDescription>
|
||||
People and bots in {channel.name}.
|
||||
|
||||
@@ -4,15 +4,15 @@ export function DayDivider({ label }: { label: string }) {
|
||||
return (
|
||||
<section
|
||||
aria-label={label}
|
||||
className="flex items-center gap-3 py-2"
|
||||
className="flex items-center gap-2.5 py-1.5"
|
||||
data-testid="message-timeline-day-divider"
|
||||
data-day-label={label}
|
||||
>
|
||||
<Separator className="flex-1" />
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.22em] text-muted-foreground">
|
||||
<Separator className="flex-1 bg-border/35" />
|
||||
<p className="text-[10px] font-medium tracking-[0.02em] text-muted-foreground/65">
|
||||
{label}
|
||||
</p>
|
||||
<Separator className="flex-1" />
|
||||
<Separator className="flex-1 bg-border/35" />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as React from "react";
|
||||
import { ArrowLeft, X } from "lucide-react";
|
||||
|
||||
import type { MainTimelineEntry } from "@/features/messages/lib/threadPanel";
|
||||
@@ -75,12 +76,11 @@ export function MessageThreadPanel({
|
||||
threadReplies,
|
||||
threadTypingPubkeys,
|
||||
}: MessageThreadPanelProps) {
|
||||
if (!threadHead) {
|
||||
return null;
|
||||
}
|
||||
const threadBodyRef = React.useRef<HTMLDivElement>(null);
|
||||
const threadContentRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const composerReplyTarget =
|
||||
replyTargetMessage && replyTargetMessage.id !== threadHead.id
|
||||
threadHead && replyTargetMessage && replyTargetMessage.id !== threadHead.id
|
||||
? {
|
||||
author: replyTargetMessage.author,
|
||||
body: replyTargetMessage.body,
|
||||
@@ -88,9 +88,52 @@ export function MessageThreadPanel({
|
||||
}
|
||||
: null;
|
||||
|
||||
const scrollThreadToBottom = React.useCallback(() => {
|
||||
const threadBody = threadBodyRef.current;
|
||||
if (!threadBody) {
|
||||
return;
|
||||
}
|
||||
|
||||
threadBody.scrollTo({
|
||||
top: threadBody.scrollHeight,
|
||||
behavior: "auto",
|
||||
});
|
||||
}, []);
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
if (!threadHead) {
|
||||
return;
|
||||
}
|
||||
scrollThreadToBottom();
|
||||
}, [scrollThreadToBottom, threadHead?.id]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!threadHead) {
|
||||
return;
|
||||
}
|
||||
|
||||
const threadContent = threadContentRef.current;
|
||||
if (!threadContent || typeof ResizeObserver === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new ResizeObserver(() => {
|
||||
scrollThreadToBottom();
|
||||
});
|
||||
|
||||
observer.observe(threadContent);
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [scrollThreadToBottom, threadHead?.id]);
|
||||
|
||||
if (!threadHead) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<aside
|
||||
className="relative z-10 hidden h-full min-h-0 w-[min(100%,420px)] shrink-0 flex-col border-l border-border/60 bg-muted/20 pt-0 lg:flex"
|
||||
className="relative z-10 hidden h-full min-h-0 w-[min(100%,420px)] shrink-0 flex-col border-l border-border/60 bg-muted/20 pt-14 lg:flex"
|
||||
data-testid="message-thread-panel"
|
||||
>
|
||||
<div className="relative z-20 flex shrink-0 items-center justify-between bg-background/25 px-2 py-1 shadow-[0_4px_24px_rgba(0,0,0,0.06)] backdrop-blur-xl supports-[backdrop-filter]:bg-background/20 dark:shadow-[0_4px_24px_rgba(0,0,0,0.25)]">
|
||||
@@ -124,53 +167,59 @@ export function MessageThreadPanel({
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="min-h-0 flex-1 overflow-y-auto overscroll-contain"
|
||||
className="min-h-0 flex-1 overflow-y-auto overscroll-contain pb-12"
|
||||
data-testid="message-thread-body"
|
||||
ref={threadBodyRef}
|
||||
>
|
||||
<div className="px-4 py-3" data-testid="message-thread-head">
|
||||
<div className="mx-auto w-full max-w-[23rem]">
|
||||
<MessageRow
|
||||
activeReplyTargetId={replyTargetId}
|
||||
message={threadHead}
|
||||
onDelete={
|
||||
onDelete && canManageMessage(threadHead, currentPubkey)
|
||||
? onDelete
|
||||
: undefined
|
||||
}
|
||||
onToggleReaction={onToggleReaction}
|
||||
profiles={profiles}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-4 pb-2 pt-3" data-testid="message-thread-replies">
|
||||
{threadReplies.length > 0 ? (
|
||||
<div className="mx-auto w-full max-w-[23rem] space-y-2">
|
||||
{threadReplies.map((entry) => (
|
||||
<div key={entry.message.id}>
|
||||
<MessageRow
|
||||
activeReplyTargetId={replyTargetId}
|
||||
message={entry.message}
|
||||
onDelete={
|
||||
onDelete && canManageMessage(entry.message, currentPubkey)
|
||||
? onDelete
|
||||
: undefined
|
||||
}
|
||||
onReply={onOpenNestedThread}
|
||||
onToggleReaction={onToggleReaction}
|
||||
profiles={profiles}
|
||||
/>
|
||||
{entry.summary ? (
|
||||
<MessageThreadSummaryRow
|
||||
message={entry.message}
|
||||
onOpenThread={onOpenNestedThread}
|
||||
summary={entry.summary}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
<div ref={threadContentRef}>
|
||||
<div className="px-4 py-3" data-testid="message-thread-head">
|
||||
<div className="mx-auto w-full max-w-[23rem]">
|
||||
<MessageRow
|
||||
activeReplyTargetId={replyTargetId}
|
||||
message={threadHead}
|
||||
onDelete={
|
||||
onDelete && canManageMessage(threadHead, currentPubkey)
|
||||
? onDelete
|
||||
: undefined
|
||||
}
|
||||
onToggleReaction={onToggleReaction}
|
||||
profiles={profiles}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="px-4 pb-2 pt-3" data-testid="message-thread-replies">
|
||||
{threadReplies.length > 0 ? (
|
||||
<div className="mx-auto w-full max-w-[23rem] space-y-2">
|
||||
{threadReplies.map((entry) => (
|
||||
<div
|
||||
key={entry.message.id}
|
||||
className="flex flex-col gap-0"
|
||||
>
|
||||
<MessageRow
|
||||
activeReplyTargetId={replyTargetId}
|
||||
message={entry.message}
|
||||
onDelete={
|
||||
onDelete && canManageMessage(entry.message, currentPubkey)
|
||||
? onDelete
|
||||
: undefined
|
||||
}
|
||||
onReply={onOpenNestedThread}
|
||||
onToggleReaction={onToggleReaction}
|
||||
profiles={profiles}
|
||||
/>
|
||||
{entry.summary ? (
|
||||
<MessageThreadSummaryRow
|
||||
message={entry.message}
|
||||
onOpenThread={onOpenNestedThread}
|
||||
summary={entry.summary}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ export function MessageThreadSummaryRow({
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
className="-mt-1 ml-8 flex w-fit max-w-full items-center gap-2 rounded-xl px-3 py-1 text-left text-sm text-muted-foreground transition-colors hover:bg-accent/50 hover:text-foreground"
|
||||
className="-mt-0.5 ml-10 flex w-fit max-w-full items-center gap-1 rounded-xl px-3 py-0.5 text-left text-sm text-muted-foreground transition-colors hover:bg-accent/50 hover:text-foreground"
|
||||
data-thread-head-id={message.id}
|
||||
data-testid="message-thread-summary"
|
||||
onClick={() => onOpenThread(message)}
|
||||
|
||||
@@ -95,23 +95,23 @@ export const MessageTimeline = React.memo(function MessageTimeline({
|
||||
<div className="relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
|
||||
{stickyDayLabel && !isAtBottom ? (
|
||||
<div
|
||||
className="pointer-events-none absolute inset-x-0 top-0 z-10 flex justify-center px-4 pt-1 sm:px-6"
|
||||
className="pointer-events-none absolute inset-x-0 top-16 z-10 flex justify-center px-4 pt-1 sm:px-6"
|
||||
data-testid="message-timeline-sticky-day"
|
||||
>
|
||||
<p className="rounded-md bg-muted/60 px-2 py-0.5 text-[11px] font-medium leading-none tracking-normal text-muted-foreground/75 shadow-sm backdrop-blur-sm">
|
||||
<p className="rounded-md border border-border/35 bg-background/45 px-2 py-0.5 text-[10px] font-medium leading-none tracking-[0.02em] text-muted-foreground/65 backdrop-blur-sm">
|
||||
{stickyDayLabel}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
className="absolute inset-0 overflow-y-auto overflow-x-hidden overscroll-contain px-4 pb-3 pt-1 [overflow-anchor:none] sm:px-6"
|
||||
className="absolute inset-0 overflow-y-auto overflow-x-hidden overscroll-contain px-4 pb-12 pt-1 [overflow-anchor:none] sm:px-6"
|
||||
data-scroll-restoration-id="message-timeline"
|
||||
data-testid="message-timeline"
|
||||
onScroll={syncScrollState}
|
||||
ref={scrollContainerRef}
|
||||
>
|
||||
<div
|
||||
className="mx-auto flex w-full max-w-4xl flex-col gap-2 pb-36 pt-10"
|
||||
className="mx-auto flex w-full max-w-4xl flex-col gap-2 pb-8 pt-14"
|
||||
ref={contentRef}
|
||||
>
|
||||
<div ref={topSentinelRef} aria-hidden className="h-px" />
|
||||
|
||||
@@ -74,6 +74,34 @@ export const TimelineMessageList = React.memo(function TimelineMessageList({
|
||||
time={message.time}
|
||||
/>,
|
||||
);
|
||||
} else if (summary && onReply) {
|
||||
elements.push(
|
||||
<div key={message.id} className="flex flex-col gap-0">
|
||||
<MessageRow
|
||||
activeReplyTargetId={activeReplyTargetId}
|
||||
highlighted={message.id === highlightedMessageId}
|
||||
message={message}
|
||||
onDelete={
|
||||
onDelete && currentPubkey && message.pubkey === currentPubkey
|
||||
? onDelete
|
||||
: undefined
|
||||
}
|
||||
onEdit={
|
||||
onEdit && currentPubkey && message.pubkey === currentPubkey
|
||||
? onEdit
|
||||
: undefined
|
||||
}
|
||||
onToggleReaction={onToggleReaction}
|
||||
onReply={onReply}
|
||||
profiles={profiles}
|
||||
/>
|
||||
<MessageThreadSummaryRow
|
||||
message={message}
|
||||
onOpenThread={onReply}
|
||||
summary={summary}
|
||||
/>
|
||||
</div>,
|
||||
);
|
||||
} else {
|
||||
elements.push(
|
||||
<MessageRow
|
||||
@@ -96,17 +124,6 @@ export const TimelineMessageList = React.memo(function TimelineMessageList({
|
||||
profiles={profiles}
|
||||
/>,
|
||||
);
|
||||
|
||||
if (summary && onReply) {
|
||||
elements.push(
|
||||
<MessageThreadSummaryRow
|
||||
key={`thread-summary-${message.id}`}
|
||||
message={message}
|
||||
onOpenThread={onReply}
|
||||
summary={summary}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import * as React from "react";
|
||||
|
||||
const STICKY_DAY_TRIGGER_OFFSET_PX = 64;
|
||||
|
||||
/**
|
||||
* Tracks the day label of the topmost visible day-divider in the scroll
|
||||
* container so a floating header can mirror it.
|
||||
@@ -27,13 +29,14 @@ export function useStickyDayHeader(
|
||||
}
|
||||
|
||||
const containerTop = container.getBoundingClientRect().top;
|
||||
const stickyTriggerTop = containerTop + STICKY_DAY_TRIGGER_OFFSET_PX;
|
||||
|
||||
// Walk dividers from the end — the last one whose top is at or above the
|
||||
// container's top edge is the "current" day.
|
||||
// Walk dividers from the end — the last one whose top has reached the
|
||||
// floating label's visual zone is the "current" day.
|
||||
let current: string | null = null;
|
||||
for (let i = dividers.length - 1; i >= 0; i--) {
|
||||
const rect = dividers[i].getBoundingClientRect();
|
||||
if (rect.top <= containerTop + 4) {
|
||||
if (rect.top <= stickyTriggerTop) {
|
||||
current = dividers[i].getAttribute("data-day-label");
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -43,10 +43,10 @@
|
||||
}
|
||||
|
||||
.rich-text-composer .tiptap pre {
|
||||
border-radius: 1rem;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid hsl(var(--border) / 0.7);
|
||||
background: hsl(var(--muted) / 0.6);
|
||||
padding: 0.75rem 1rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
overflow-x: auto;
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ function createMarkdownComponents(
|
||||
return <p className={paragraphClassName}>{children}</p>;
|
||||
},
|
||||
pre: ({ children }) => (
|
||||
<pre className="overflow-x-auto rounded-2xl border border-border/70 bg-muted/60 px-4 py-3 shadow-sm">
|
||||
<pre className="overflow-x-auto rounded-xl border border-border/70 bg-muted/60 px-3 py-1.5 shadow-sm">
|
||||
{children}
|
||||
</pre>
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user