fix(desktop): continuous blur across inbox header columns

Render a single full-width backdrop strip behind the inbox pane headers
instead of a per-pane backdrop-filter box. The blur now samples
continuously across the column boundary rather than clipping at each
pane edge. Inset headers become transparent and share a measured height
via a new CSS variable so the strip spans both columns.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Thomas Petersen
2026-06-13 13:35:48 -04:00
parent d9ce0943ed
commit 916f57891e
5 changed files with 89 additions and 10 deletions

View File

@@ -40,7 +40,12 @@ import { resolveUserLabel } from "@/features/profile/lib/identity";
import { deleteMessage, sendChannelMessage } from "@/shared/api/tauri";
import type { HomeFeedResponse } from "@/shared/api/types";
import { KIND_REACTION } from "@/shared/constants/kinds";
import { topChromeInset } from "@/shared/layout/chromeLayout";
import {
insetHeaderHeightMeasurement,
insetHeaderOverlay,
topChromeInset,
} from "@/shared/layout/chromeLayout";
import { useMeasuredCssVariable } from "@/shared/layout/useMeasuredCssVariable";
import { cn } from "@/shared/lib/cn";
import { resolveMentionNames } from "@/shared/lib/resolveMentionNames";
import { useElementWidth } from "@/shared/hooks/use-mobile";
@@ -68,6 +73,12 @@ export function HomeView({
onRefresh,
}: HomeViewProps) {
const [homeInboxRef, homeInboxWidthPx] = useElementWidth<HTMLDivElement>();
// One pane header drives the shared backdrop strip height so the blur is a
// single element spanning both columns instead of clipping per pane.
const headerChromeRef = useMeasuredCssVariable({
targetRef: homeInboxRef,
...insetHeaderHeightMeasurement,
});
const isNarrowHomeViewport =
homeInboxWidthPx > 0 &&
homeInboxWidthPx < INBOX_SINGLE_COLUMN_BREAKPOINT_PX;
@@ -348,10 +359,13 @@ export function HomeView({
} as React.CSSProperties
}
>
<div aria-hidden className={insetHeaderOverlay.backdrop} />
{showListPane ? (
<InboxListPane
doneSet={effectiveDoneSet}
filter={filter}
headerChromeRef={headerChromeRef}
items={filteredItems}
onFilterChange={setFilter}
onSelect={(itemId) => {
@@ -398,6 +412,7 @@ export function HomeView({
contextChannelName={selectedChannel?.name ?? null}
currentPubkey={currentPubkey}
disabledReplyReason={disabledReplyReason}
headerChromeRef={showListPane ? undefined : headerChromeRef}
isDeletingMessage={isDeletingMessage}
isDone={
selectedItem ? effectiveDoneSet.has(selectedItem.id) : false

View File

@@ -24,6 +24,7 @@ import type { TimelineMessage } from "@/features/messages/types";
import { MessageComposer } from "@/features/messages/ui/MessageComposer";
import { UpdateIndicator } from "@/features/settings/UpdateIndicator";
import type { Channel } from "@/shared/api/types";
import { insetHeaderOverlay } from "@/shared/layout/chromeLayout";
import { TopChromeInsetHeader } from "@/shared/layout/TopChromeInsetHeader";
import { cn } from "@/shared/lib/cn";
import { Button } from "@/shared/ui/button";
@@ -46,6 +47,8 @@ type InboxDetailPaneProps = {
canOpenChannel: boolean;
canReply: boolean;
disabledReplyReason?: string | null;
/** Measured ref wiring the header height to the shared backdrop strip. */
headerChromeRef?: React.Ref<HTMLDivElement>;
isDone: boolean;
isDeletingMessage?: boolean;
isSendingReply?: boolean;
@@ -80,6 +83,7 @@ export function InboxDetailPane({
canOpenChannel,
canReply,
disabledReplyReason,
headerChromeRef,
isDone,
isDeletingMessage = false,
isSendingReply = false,
@@ -234,7 +238,11 @@ export function InboxDetailPane({
ref={detailPaneRef}
>
<div className="relative flex min-h-0 flex-1 flex-col overflow-hidden">
<TopChromeInsetHeader>
<TopChromeInsetHeader
className={insetHeaderOverlay.negativeMargin}
ref={headerChromeRef}
transparent
>
<div className="px-5 py-1 pr-3">
<div className="flex min-w-0 items-center justify-between gap-3">
<div
@@ -316,7 +324,12 @@ export function InboxDetailPane({
</div>
</TopChromeInsetHeader>
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain pb-32">
<div
className={cn(
"min-h-0 flex-1 overflow-y-auto overscroll-contain pb-32",
insetHeaderOverlay.contentPadding,
)}
>
<div>
{isThreadContextLoading ? (
<div className="px-6 pb-3 text-[11px] text-muted-foreground">

View File

@@ -1,11 +1,15 @@
import { ChevronDown, Inbox } from "lucide-react";
import type * as React from "react";
import {
formatInboxTypeLabel,
type InboxFilter,
type InboxItem,
} from "@/features/home/lib/inbox";
import { topChromeInset } from "@/shared/layout/chromeLayout";
import {
insetHeaderOverlay,
topChromeInset,
} from "@/shared/layout/chromeLayout";
import { TopChromeInsetHeader } from "@/shared/layout/TopChromeInsetHeader";
import { cn } from "@/shared/lib/cn";
import { Button } from "@/shared/ui/button";
@@ -30,6 +34,8 @@ const FILTER_OPTIONS: Array<{ label: string; value: InboxFilter }> = [
type InboxListPaneProps = {
doneSet: ReadonlySet<string>;
filter: InboxFilter;
/** Measured ref wiring the header height to the shared backdrop strip. */
headerChromeRef?: React.Ref<HTMLDivElement>;
items: InboxItem[];
onFilterChange: (filter: InboxFilter) => void;
onSelect: (itemId: string) => void;
@@ -40,6 +46,7 @@ type InboxListPaneProps = {
export function InboxListPane({
doneSet,
filter,
headerChromeRef,
items,
onFilterChange,
onSelect,
@@ -55,7 +62,11 @@ export function InboxListPane({
showRightDivider && topChromeInset.verticalDivider,
)}
>
<TopChromeInsetHeader>
<TopChromeInsetHeader
className={insetHeaderOverlay.negativeMargin}
ref={headerChromeRef}
transparent
>
<div className="px-5 py-1">
<div className="flex min-w-0 items-center justify-between gap-3">
<div className="flex min-w-0 items-center gap-[6px]">
@@ -99,7 +110,10 @@ export function InboxListPane({
</TopChromeInsetHeader>
<div
className="min-h-0 flex-1 overflow-y-auto overscroll-contain"
className={cn(
"min-h-0 flex-1 overflow-y-auto overscroll-contain",
insetHeaderOverlay.contentPadding,
)}
data-testid="home-inbox-list"
>
{items.length === 0 ? (

View File

@@ -3,7 +3,14 @@ import type * as React from "react";
import { topChromeInset } from "@/shared/layout/chromeLayout";
import { cn } from "@/shared/lib/cn";
type TopChromeInsetHeaderProps = React.ComponentProps<"div">;
type TopChromeInsetHeaderProps = React.ComponentProps<"div"> & {
/**
* Render without its own backdrop, borders, and hairlines — for headers
* stacked on a shared `insetHeaderOverlay.backdrop` strip that spans
* multiple columns (keeps the blur continuous across pane boundaries).
*/
transparent?: boolean;
};
/**
* Flowed header row that clears the global search/drag chrome and draws the
@@ -12,14 +19,16 @@ type TopChromeInsetHeaderProps = React.ComponentProps<"div">;
export function TopChromeInsetHeader({
className,
children,
transparent = false,
...props
}: TopChromeInsetHeaderProps) {
return (
<div
className={cn(
topChromeInset.headerBase,
transparent
? "relative z-40 shrink-0"
: cn(topChromeInset.headerBase, topChromeInset.divider),
topChromeInset.padding,
topChromeInset.divider,
className,
)}
{...props}

View File

@@ -1,14 +1,17 @@
export const TOP_CHROME_HEIGHT_DEFAULT = "2.5rem";
export const CHANNEL_CONTENT_TOP_PADDING_DEFAULT = "5.75rem";
export const INSET_HEADER_HEIGHT_DEFAULT = "5rem";
export const chromeCssVars = {
topChromeHeight: "--buzz-top-chrome-height",
channelContentTopPadding: "--buzz-channel-content-top-padding",
insetHeaderHeight: "--buzz-inset-header-height",
} as const;
export const chromeCssVarDefaults = {
[chromeCssVars.topChromeHeight]: TOP_CHROME_HEIGHT_DEFAULT,
[chromeCssVars.channelContentTopPadding]: CHANNEL_CONTENT_TOP_PADDING_DEFAULT,
[chromeCssVars.insetHeaderHeight]: INSET_HEADER_HEIGHT_DEFAULT,
} as const;
export const channelContentTopPaddingMeasurement = {
@@ -16,6 +19,31 @@ export const channelContentTopPaddingMeasurement = {
resetValue: chromeCssVarDefaults[chromeCssVars.channelContentTopPadding],
} as const;
export const insetHeaderHeightMeasurement = {
cssVariable: chromeCssVars.insetHeaderHeight,
resetValue: chromeCssVarDefaults[chromeCssVars.insetHeaderHeight],
} as const;
/**
* Tailwind class fragments for a flowed `TopChromeInsetHeader` that overlays
* the scrollable content below it, so the content scrolls under the
* translucent blurred header (same treatment as the channel header).
*/
export const insetHeaderOverlay = {
/** Negative bottom margin pulling the next sibling under the header. */
negativeMargin: "-mb-(--buzz-inset-header-height,5rem)",
/** Padding-top reserving the measured header height inside the scroll area. */
contentPadding: "pt-(--buzz-inset-header-height,5rem)",
/**
* Single full-width backdrop strip drawn behind transparent inset headers.
* Rendered once per view so the blur samples continuously across column
* boundaries instead of clipping at each pane's backdrop-filter box.
* Carries the search-strip hairline (`before:`) and the bottom border.
*/
backdrop:
"pointer-events-none absolute inset-x-0 top-0 z-30 h-(--buzz-inset-header-height,5rem) border-b border-border/35 bg-background/80 backdrop-blur-md supports-backdrop-filter:bg-background/70 dark:bg-background/70 dark:backdrop-blur-xl dark:supports-backdrop-filter:bg-background/55 before:pointer-events-none before:absolute before:inset-x-0 before:top-(--buzz-top-chrome-height,2.5rem) before:h-px before:bg-border/35 before:content-['']",
} as const;
/** Tailwind class fragments for layout under the global top chrome. */
export const topChromeInset = {
/** Absolute/fixed top offset below the search bar. */
@@ -29,7 +57,7 @@ export const topChromeInset = {
"before:pointer-events-none before:absolute before:inset-x-0 before:top-(--buzz-top-chrome-height,2.5rem) before:h-px before:bg-border/35 before:content-['']",
/** Shared header backdrop and bottom border below the inset row. */
headerBase:
"relative z-40 shrink-0 border-b border-border/35 bg-background/75 backdrop-blur-md supports-backdrop-filter:bg-background/65 dark:bg-background/45 dark:backdrop-blur-xl dark:supports-backdrop-filter:bg-background/35",
"relative z-40 shrink-0 border-b border-border/35 bg-background/80 backdrop-blur-md supports-backdrop-filter:bg-background/70 dark:bg-background/70 dark:backdrop-blur-xl dark:supports-backdrop-filter:bg-background/55",
/** Vertical pane divider starting below the global top chrome. */
verticalDivider:
"after:pointer-events-none after:absolute after:bottom-0 after:right-0 after:top-(--buzz-top-chrome-height,2.5rem) after:z-40 after:w-px after:bg-border/35 after:content-['']",