Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 0 additions & 17 deletions desktop/src/app/AppTopChrome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,6 @@ type AppTopChromeProps = {
searchLoading?: boolean;
};

function GlobalTopDivider() {
const sidebar = useOptionalSidebar();
const state = sidebar?.state ?? "collapsed";

return (
<div
aria-hidden="true"
className="pointer-events-none fixed top-10 z-40 h-px bg-border/35"
style={{
left: state === "expanded" ? "var(--sidebar-width)" : 0,
right: 0,
}}
/>
);
}

function CenterColumnTopbarSearch({
channels,
currentPubkey,
Expand Down Expand Up @@ -141,7 +125,6 @@ export function AppTopChrome({
className="fixed inset-x-0 top-0 z-20 h-10 cursor-default select-none"
data-tauri-drag-region
/>
<GlobalTopDivider />
<div className="fixed left-[80px] top-[6px] z-45 flex items-center gap-0.5">
<TopChromeSidebarTrigger />
<Button
Expand Down
5 changes: 4 additions & 1 deletion desktop/src/features/channels/ui/ChannelPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -603,7 +603,10 @@ export const ChannelPane = React.memo(function ChannelPane({
[agentSessionAgents, openAgentSessionPubkey],
);
return (
<div className="flex min-h-0 min-w-0 flex-1 flex-row overflow-hidden">
<div className="relative flex min-h-0 min-w-0 flex-1 flex-row overflow-hidden">
{!isSinglePanelView ? (
<div aria-hidden="true" className={channelChrome.backdrop} />
) : null}
{!isSinglePanelView ? (
<section
aria-label="Channel messages and composer"
Expand Down
3 changes: 3 additions & 0 deletions desktop/src/features/channels/ui/ChannelScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,8 @@ export function ChannelScreen({
resetKey: activeChannelId,
enabled: !isSinglePanelView,
});
const useSharedChannelHeaderBackdrop =
activeChannel?.channelType !== "forum" && !isSinglePanelView;
React.useEffect(() => {
setTopbarSearchHidden(isSinglePanelView);
return () => {
Expand All @@ -614,6 +616,7 @@ export function ChannelScreen({
onManageChannel={openChannelManagement}
onToggleMembers={() => setIsMembersSidebarOpen((prev) => !prev)}
showHeaderContent={!isSinglePanelView}
transparentChrome={useSharedChannelHeaderBackdrop}
/>
);

Expand Down
3 changes: 3 additions & 0 deletions desktop/src/features/channels/ui/ChannelScreenHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type ChannelScreenHeaderProps = {
isAddBotOpen?: boolean;
isJoining?: boolean;
showHeaderContent?: boolean;
transparentChrome?: boolean;
onAddBotOpenChange?: (open: boolean) => void;
onJoinChannel?: () => Promise<void>;
onManageChannel: () => void;
Expand All @@ -46,6 +47,7 @@ export function ChannelScreenHeader({
isJoining = false,
onAddBotOpenChange,
showHeaderContent = true,
transparentChrome = false,
onJoinChannel,
onManageChannel,
onToggleMembers,
Expand Down Expand Up @@ -121,6 +123,7 @@ export function ChannelScreenHeader({
/>
}
title={activeChannelTitle}
transparentChrome={transparentChrome}
visibility={activeChannel?.visibility}
/>
);
Expand Down
6 changes: 5 additions & 1 deletion desktop/src/features/chat/ui/ChatHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type ChatHeaderProps = {
mode?: "home" | "channel" | "agents" | "workflows" | "pulse" | "projects";
overlaysContent?: boolean;
statusBadge?: React.ReactNode;
transparentChrome?: boolean;
};

const HEADER_ICON_CLASS = "h-4 w-4 text-muted-foreground";
Expand Down Expand Up @@ -92,6 +93,7 @@ export function ChatHeader({
mode = "channel",
overlaysContent = false,
statusBadge,
transparentChrome = false,
}: ChatHeaderProps) {
const trimmedDescription = description?.trim() ?? "";

Expand Down Expand Up @@ -150,7 +152,9 @@ export function ChatHeader({
<div
ref={chromeWrapperRef}
className={cn(
"pointer-events-none relative z-30 bg-background/80 backdrop-blur-md after:absolute after:inset-x-0 after:bottom-0 after:h-px after:bg-border/35 after:content-[''] supports-backdrop-filter:bg-background/70 dark:bg-background/70 dark:backdrop-blur-xl dark:supports-backdrop-filter:bg-background/55",
transparentChrome
? "pointer-events-none relative z-40"
: "pointer-events-none relative z-30 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",
topChromeInset.padding,
channelChrome.negativeMargin,
)}
Expand Down
17 changes: 16 additions & 1 deletion desktop/src/features/home/ui/HomeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,12 @@ import {
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";
Expand Down Expand Up @@ -75,6 +80,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;
Expand Down Expand Up @@ -405,11 +416,14 @@ export function HomeView({
} as React.CSSProperties
}
>
<div aria-hidden className={insetHeaderOverlay.backdrop} />

{showListPane ? (
<InboxListPane
doneSet={effectiveDoneSet}
dueReminderCount={dueReminderCount}
filter={filter}
headerChromeRef={headerChromeRef}
items={filteredItems}
onFilterChange={setFilter}
onSelect={(itemId) => {
Expand Down Expand Up @@ -457,6 +471,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
Expand Down
17 changes: 15 additions & 2 deletions desktop/src/features/home/ui/InboxDetailPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
Expand Down Expand Up @@ -80,6 +83,7 @@ export function InboxDetailPane({
canOpenChannel,
canReply,
disabledReplyReason,
headerChromeRef,
isDone,
isDeletingMessage = false,
isSendingReply = false,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -310,7 +318,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-2xs text-muted-foreground">
Expand Down
19 changes: 16 additions & 3 deletions desktop/src/features/home/ui/InboxListPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import {
type InboxItem,
} from "@/features/home/lib/inbox";
import { RemindersPanel } from "@/features/reminders/ui/RemindersPanel";
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";
Expand All @@ -34,6 +37,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;
Expand All @@ -46,6 +51,7 @@ type InboxListPaneProps = {
export function InboxListPane({
doneSet,
filter,
headerChromeRef,
items,
onFilterChange,
onSelect,
Expand Down Expand Up @@ -149,7 +155,11 @@ export function InboxListPane({
showRightDivider && topChromeInset.verticalDivider,
)}
>
<TopChromeInsetHeader>
<TopChromeInsetHeader
className={insetHeaderOverlay.negativeMargin}
ref={headerChromeRef}
transparent
>
<div className="px-5 py-1">
{/* Cap to the list-column width so the right-aligned dropdown stays
put when the pane goes full-width in reminders mode. */}
Expand Down Expand Up @@ -219,7 +229,10 @@ export function InboxListPane({
</div>
) : (
<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"
ref={scrollRef}
>
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/shared/layout/AuxiliaryPanelHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function AuxiliaryPanelHeader({
return (
<div
className={cn(
"pointer-events-none relative z-30 bg-background/80 backdrop-blur-md after:absolute after:inset-x-0 after:bottom-0 after:h-px after:bg-border/35 after:content-[''] supports-backdrop-filter:bg-background/70 dark:bg-background/70 dark:backdrop-blur-xl dark:supports-backdrop-filter:bg-background/55",
"pointer-events-none relative z-40",
topChromeInset.padding,
channelChrome.negativeMargin,
className,
Expand Down
15 changes: 12 additions & 3 deletions desktop/src/shared/layout/TopChromeInsetHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}
Expand Down
39 changes: 36 additions & 3 deletions desktop/src/shared/layout/chromeLayout.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,50 @@
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 = {
cssVariable: chromeCssVars.channelContentTopPadding,
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.
* Keeps the inset header visually continuous without drawing horizontal
* dividers across the header area.
*/
backdrop:
"pointer-events-none absolute inset-x-0 top-0 z-30 h-(--buzz-inset-header-height,5rem) bg-background/90 backdrop-blur-md supports-backdrop-filter:bg-background/85 dark:bg-background/70 dark:backdrop-blur-xl dark:supports-backdrop-filter:bg-background/55",
} as const;

/** Tailwind class fragments for layout under the global top chrome. */
export const topChromeInset = {
/** Absolute/fixed top offset below the search bar. */
Expand All @@ -29,7 +58,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-['']",
Expand All @@ -39,14 +68,18 @@ export const topChromeInset = {
export const topChromeBackdrop = {
/** Height matching the global top chrome search/drag strip. */
height: "h-(--buzz-top-chrome-height,2.5rem)",
/** `after:` pseudo-element offset aligned to the bottom of top chrome. */
dividerTop: "after:top-(--buzz-top-chrome-height,2.5rem)",
} as const;

/** Tailwind class fragments for measured channel header chrome. */
export const channelChrome = {
/** Padding-top that clears the measured channel header chrome. */
contentPadding: "pt-(--buzz-channel-content-top-padding,5.75rem)",
/**
* Single full-width backdrop strip behind channel and auxiliary headers so
* backdrop blur samples continuously across column boundaries.
*/
backdrop:
"pointer-events-none absolute inset-x-0 top-0 z-30 h-(--buzz-channel-content-top-padding,5.75rem) bg-background/90 backdrop-blur-md supports-backdrop-filter:bg-background/85 dark:bg-background/70 dark:backdrop-blur-xl dark:supports-backdrop-filter:bg-background/55",
/** Absolute/fixed top offset below the measured channel header chrome. */
top: "top-(--buzz-channel-content-top-padding,5.75rem)",
/** Height matching the measured channel header chrome. */
Expand Down
Loading