Polish side panel motion and composer alignment (#1294)

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
klopez4212
2026-06-26 02:16:56 +01:00
committed by GitHub
parent 59b21592c9
commit 34b2d3e8b4
13 changed files with 81 additions and 22 deletions

View File

@@ -801,7 +801,7 @@ export function AppShell() {
className="isolate min-h-0 min-w-0 overflow-hidden bg-sidebar" className="isolate min-h-0 min-w-0 overflow-hidden bg-sidebar"
style={chromeCssVarDefaults} style={chromeCssVarDefaults}
> >
<div className="relative z-10 ml-px mt-px flex min-h-0 flex-1 flex-col overflow-hidden rounded-tl-xl bg-background shadow-[-1px_-1px_0_0_hsl(var(--sidebar-border)/0.45)]"> <div className="relative z-10 mb-2 ml-px mr-2 mt-px flex min-h-0 flex-1 flex-col overflow-hidden rounded-xl bg-background shadow-[-1px_-1px_0_0_hsl(var(--sidebar-border)/0.45)]">
<ConnectionBanner <ConnectionBanner
errorMessage={channelsErrorMessage} errorMessage={channelsErrorMessage}
/> />

View File

@@ -1,3 +1,4 @@
import * as React from "react";
import { import {
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
@@ -22,6 +23,10 @@ type AppTopChromeProps = {
const TOP_CHROME_ICON_BUTTON_CLASS = const TOP_CHROME_ICON_BUTTON_CLASS =
"h-7 w-7 rounded-[4px] text-sidebar-foreground/65 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [&_svg]:size-4"; "h-7 w-7 rounded-[4px] text-sidebar-foreground/65 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [&_svg]:size-4";
function preventTopChromeWheel(event: WheelEvent) {
event.preventDefault();
}
function TopChromeSidebarTrigger() { function TopChromeSidebarTrigger() {
const sidebar = useOptionalSidebar(); const sidebar = useOptionalSidebar();
@@ -50,6 +55,7 @@ export function AppTopChrome({
onGoBack, onGoBack,
onGoForward, onGoForward,
}: AppTopChromeProps) { }: AppTopChromeProps) {
const topChromeRef = React.useRef<HTMLDivElement>(null);
const isFullscreen = useIsFullscreen(); const isFullscreen = useIsFullscreen();
// On macOS the traffic-light buttons overlay the chrome (see // On macOS the traffic-light buttons overlay the chrome (see
// `trafficLightPosition` in `tauri.conf.json`), so the nav row clears their // `trafficLightPosition` in `tauri.conf.json`), so the nav row clears their
@@ -60,14 +66,29 @@ export function AppTopChrome({
const navRowAlignmentClass = const navRowAlignmentClass =
isMacPlatform() && !isFullscreen ? "translate-y-[3px]" : null; isMacPlatform() && !isFullscreen ? "translate-y-[3px]" : null;
React.useEffect(() => {
const topChrome = topChromeRef.current;
if (!topChrome) {
return;
}
const options = { capture: true, passive: false };
topChrome.addEventListener("wheel", preventTopChromeWheel, options);
return () => {
topChrome.removeEventListener("wheel", preventTopChromeWheel, options);
};
}, []);
return ( return (
<div <div
ref={topChromeRef}
className={cn( className={cn(
"relative z-45 flex shrink-0 cursor-default select-none items-center bg-sidebar pr-3 text-sidebar-foreground", "relative z-45 flex shrink-0 cursor-default select-none items-center bg-sidebar pr-3 text-sidebar-foreground",
topChromeBackdrop.height, topChromeBackdrop.height,
navRowPaddingClass, navRowPaddingClass,
)} )}
data-tauri-drag-region data-tauri-drag-region
data-testid="app-top-chrome"
> >
<div className={cn("flex items-center gap-0.5", navRowAlignmentClass)}> <div className={cn("flex items-center gap-0.5", navRowAlignmentClass)}>
<TopChromeSidebarTrigger /> <TopChromeSidebarTrigger />

View File

@@ -20,7 +20,7 @@ import { Button } from "@/shared/ui/button";
import type { UserProfileLookup } from "@/features/profile/lib/identity"; import type { UserProfileLookup } from "@/features/profile/lib/identity";
import { import {
OverlayPanelBackdrop, OverlayPanelBackdrop,
PANEL_BASE_CLASS, PANEL_ENTER_BASE_CLASS,
PANEL_OVERLAY_CLASS, PANEL_OVERLAY_CLASS,
PANEL_SINGLE_COLUMN_HEADER_LAYER_CLASS, PANEL_SINGLE_COLUMN_HEADER_LAYER_CLASS,
} from "@/shared/ui/OverlayPanelBackdrop"; } from "@/shared/ui/OverlayPanelBackdrop";
@@ -179,7 +179,7 @@ export function AgentSessionThreadPanel({
{isFloatingOverlay && <OverlayPanelBackdrop onClose={onClose} />} {isFloatingOverlay && <OverlayPanelBackdrop onClose={onClose} />}
<aside <aside
className={cn( className={cn(
PANEL_BASE_CLASS, PANEL_ENTER_BASE_CLASS,
isSinglePanelView && "border-l-0", isSinglePanelView && "border-l-0",
isFloatingOverlay && PANEL_OVERLAY_CLASS, isFloatingOverlay && PANEL_OVERLAY_CLASS,
)} )}

View File

@@ -33,6 +33,7 @@ export function ChannelManagementAuxiliaryPanel({
}: ChannelManagementAuxiliaryPanelProps) { }: ChannelManagementAuxiliaryPanelProps) {
const panel = ( const panel = (
<ChannelManagementSheet <ChannelManagementSheet
animateSplitEnter={isSinglePanelView && !useSplitAuxiliaryPane}
channel={activeChannel} channel={activeChannel}
currentPubkey={currentPubkey} currentPubkey={currentPubkey}
layout={useSplitAuxiliaryPane || isSinglePanelView ? "split" : "overlay"} layout={useSplitAuxiliaryPane || isSinglePanelView ? "split" : "overlay"}

View File

@@ -62,6 +62,7 @@ import { useScrollBoundaryLock } from "@/shared/hooks/useScrollBoundaryLock";
import { import {
OverlayPanelBackdrop, OverlayPanelBackdrop,
PANEL_BASE_CLASS, PANEL_BASE_CLASS,
PANEL_ENTER_MOTION_CLASS,
PANEL_OVERLAY_CLASS, PANEL_OVERLAY_CLASS,
} from "@/shared/ui/OverlayPanelBackdrop"; } from "@/shared/ui/OverlayPanelBackdrop";
import { ChannelCanvas } from "./ChannelCanvas"; import { ChannelCanvas } from "./ChannelCanvas";
@@ -81,6 +82,7 @@ import { ChannelManagementModerationActions } from "./ChannelManagementModeratio
type ChannelManagementSheetProps = { type ChannelManagementSheetProps = {
channel: Channel | null; channel: Channel | null;
animateSplitEnter?: boolean;
currentPubkey?: string; currentPubkey?: string;
layout?: "overlay" | "split"; layout?: "overlay" | "split";
onDeleted?: () => void; onDeleted?: () => void;
@@ -91,6 +93,7 @@ type ChannelManagementSheetProps = {
const DEFAULT_EPHEMERAL_TTL_SECONDS = 7 * 24 * 60 * 60; const DEFAULT_EPHEMERAL_TTL_SECONDS = 7 * 24 * 60 * 60;
export function ChannelManagementSheet({ export function ChannelManagementSheet({
animateSplitEnter = false,
channel, channel,
currentPubkey, currentPubkey,
layout = "overlay", layout = "overlay",
@@ -315,6 +318,7 @@ export function ChannelManagementSheet({
className={cn( className={cn(
PANEL_BASE_CLASS, PANEL_BASE_CLASS,
"h-full w-full cursor-default overflow-hidden border-l-0 p-0", "h-full w-full cursor-default overflow-hidden border-l-0 p-0",
animateSplitEnter && PANEL_ENTER_MOTION_CLASS,
isDark isDark
? "bg-background/85 backdrop-blur-xl supports-[backdrop-filter]:bg-background/75" ? "bg-background/85 backdrop-blur-xl supports-[backdrop-filter]:bg-background/75"
: "bg-background", : "bg-background",
@@ -361,7 +365,8 @@ export function ChannelManagementSheet({
className={cn( className={cn(
PANEL_BASE_CLASS, PANEL_BASE_CLASS,
PANEL_OVERLAY_CLASS, PANEL_OVERLAY_CLASS,
"w-[380px] cursor-default overflow-hidden p-0 transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right", PANEL_ENTER_MOTION_CLASS,
"w-[380px] cursor-default overflow-hidden p-0 data-[state=closed]:animate-out data-[state=closed]:slide-out-to-right data-[state=closed]:duration-200",
isDark isDark
? "bg-background/85 backdrop-blur-xl supports-[backdrop-filter]:bg-background/75" ? "bg-background/85 backdrop-blur-xl supports-[backdrop-filter]:bg-background/75"
: "bg-background", : "bg-background",

View File

@@ -794,7 +794,7 @@ export const ChannelPane = React.memo(function ChannelPane({
{hasTypingActivity ? ( {hasTypingActivity ? (
<TypingIndicatorRow <TypingIndicatorRow
channel={activeChannel} channel={activeChannel}
className="min-w-0 flex-1 px-0 py-0" className="min-w-0 flex-1 py-0 pl-[calc(0.75rem+1px)] pr-0 sm:pl-[calc(1rem+1px)]"
currentPubkey={currentPubkey} currentPubkey={currentPubkey}
profiles={profiles} profiles={profiles}
typingPubkeys={typingPubkeys} typingPubkeys={typingPubkeys}

View File

@@ -1,6 +1,8 @@
import type * as React from "react"; import type * as React from "react";
import { THREAD_PANEL_MIN_WIDTH_PX } from "@/shared/hooks/useThreadPanelWidth"; import { THREAD_PANEL_MIN_WIDTH_PX } from "@/shared/hooks/useThreadPanelWidth";
import { cn } from "@/shared/lib/cn";
import { PANEL_ENTER_MOTION_CLASS } from "@/shared/ui/OverlayPanelBackdrop";
type RightAuxiliaryPaneProps = { type RightAuxiliaryPaneProps = {
canResetWidth: boolean; canResetWidth: boolean;
@@ -23,7 +25,10 @@ export function RightAuxiliaryPane({
}: RightAuxiliaryPaneProps) { }: RightAuxiliaryPaneProps) {
return ( return (
<aside <aside
className="group/right-pane relative flex h-full shrink-0 flex-col overflow-hidden bg-background before:pointer-events-none before:absolute before:bottom-0 before:left-0 before:top-0 before:z-40 before:w-px before:bg-border/80 before:content-['']" className={cn(
"group/right-pane relative flex h-full shrink-0 flex-col overflow-hidden bg-background before:pointer-events-none before:absolute before:bottom-0 before:left-0 before:top-0 before:z-40 before:w-px before:bg-border/80 before:content-['']",
PANEL_ENTER_MOTION_CLASS,
)}
data-testid={testId} data-testid={testId}
style={{ style={{
maxWidth: constrainToAvailableSpace maxWidth: constrainToAvailableSpace

View File

@@ -104,7 +104,7 @@ export const MessageComposerToolbar = React.memo(
editor={editor} editor={editor}
onLinkButton={onLinkButton} onLinkButton={onLinkButton}
/> />
<div className="flex min-h-10 min-w-0 flex-1 items-center gap-1 py-1"> <div className="-ml-2 flex min-h-10 min-w-0 flex-1 items-center gap-1 py-1">
{/* {/*
* AnimatePresence with mode="popLayout" — exiting elements * AnimatePresence with mode="popLayout" — exiting elements
* are popped out of flow immediately so entering elements * are popped out of flow immediately so entering elements

View File

@@ -23,7 +23,7 @@ import {
import { Button } from "@/shared/ui/button"; import { Button } from "@/shared/ui/button";
import { import {
OverlayPanelBackdrop, OverlayPanelBackdrop,
PANEL_BASE_CLASS, PANEL_ENTER_BASE_CLASS,
PANEL_OVERLAY_CLASS, PANEL_OVERLAY_CLASS,
PANEL_SINGLE_COLUMN_HEADER_LAYER_CLASS, PANEL_SINGLE_COLUMN_HEADER_LAYER_CLASS,
} from "@/shared/ui/OverlayPanelBackdrop"; } from "@/shared/ui/OverlayPanelBackdrop";
@@ -207,7 +207,7 @@ function ThreadComposerSkeleton() {
</div> </div>
<div <div
className={cn( className={cn(
"-mt-1 h-7 bg-background pb-1 pt-0", "h-7 bg-background pb-1 pt-0",
THREAD_PANEL_COMPOSER_GUTTER_CLASS, THREAD_PANEL_COMPOSER_GUTTER_CLASS,
)} )}
/> />
@@ -304,7 +304,7 @@ export function MessageThreadPanelSkeleton({
{isFloatingOverlay && <OverlayPanelBackdrop onClose={onClose} />} {isFloatingOverlay && <OverlayPanelBackdrop onClose={onClose} />}
<aside <aside
className={cn( className={cn(
PANEL_BASE_CLASS, PANEL_ENTER_BASE_CLASS,
isSinglePanelView && "border-l-0", isSinglePanelView && "border-l-0",
isFloatingOverlay && PANEL_OVERLAY_CLASS, isFloatingOverlay && PANEL_OVERLAY_CLASS,
)} )}
@@ -902,7 +902,7 @@ export function MessageThreadPanel({
/> />
<div <div
className={cn( className={cn(
"-mt-1 h-7 bg-background pb-1 pt-0", "h-7 bg-background pb-1 pt-0",
THREAD_PANEL_COMPOSER_GUTTER_CLASS, THREAD_PANEL_COMPOSER_GUTTER_CLASS,
)} )}
> >
@@ -913,7 +913,7 @@ export function MessageThreadPanel({
{threadTypingPubkeys.length > 0 ? ( {threadTypingPubkeys.length > 0 ? (
<TypingIndicatorRow <TypingIndicatorRow
channel={channel} channel={channel}
className="min-w-0 flex-1 px-0 py-0" className="min-w-0 flex-1 py-0 pl-[calc(0.75rem+1px)] pr-0 sm:pl-[calc(1rem+1px)]"
currentPubkey={currentPubkey} currentPubkey={currentPubkey}
profiles={profiles} profiles={profiles}
typingPubkeys={threadTypingPubkeys} typingPubkeys={threadTypingPubkeys}
@@ -974,7 +974,7 @@ export function MessageThreadPanel({
{isFloatingOverlay && <OverlayPanelBackdrop onClose={onClose} />} {isFloatingOverlay && <OverlayPanelBackdrop onClose={onClose} />}
<aside <aside
className={cn( className={cn(
PANEL_BASE_CLASS, PANEL_ENTER_BASE_CLASS,
isSinglePanelView && "border-l-0", isSinglePanelView && "border-l-0",
isFloatingOverlay && PANEL_OVERLAY_CLASS, isFloatingOverlay && PANEL_OVERLAY_CLASS,
)} )}

View File

@@ -90,8 +90,8 @@ export function TypingIndicatorRow({
{labels.length > 0 && ( {labels.length > 0 && (
<div <div
className={cn( className={cn(
"flex w-full items-center gap-2", "flex min-w-0 w-full items-center",
isActivityVariant && "h-full", isActivityVariant ? "h-full gap-1.5" : "gap-2",
)} )}
> >
<div className="flex shrink-0 items-center"> <div className="flex shrink-0 items-center">
@@ -103,7 +103,7 @@ export function TypingIndicatorRow({
key={pubkey} key={pubkey}
className={cn( className={cn(
"relative shrink-0 rounded-lg ring-1 ring-background", "relative shrink-0 rounded-lg ring-1 ring-background",
isActivityVariant ? "h-[18px] w-[18px]" : "h-5 w-5", isActivityVariant ? "h-4 w-4" : "h-5 w-5",
index > 0 && "-ml-1.5", index > 0 && "-ml-1.5",
)} )}
data-testid="message-typing-avatar" data-testid="message-typing-avatar"
@@ -113,7 +113,7 @@ export function TypingIndicatorRow({
label={label} label={label}
className={cn( className={cn(
isActivityVariant isActivityVariant
? "h-[18px] w-[18px] text-3xs" ? "h-4 w-4 text-3xs"
: "h-5 w-5 text-3xs", : "h-5 w-5 text-3xs",
)} )}
iconClassName={ iconClassName={
@@ -126,10 +126,10 @@ export function TypingIndicatorRow({
</div> </div>
<p <p
className={cn( className={cn(
"truncate text-muted-foreground", "min-w-0 translate-y-px truncate text-muted-foreground",
isActivityVariant isActivityVariant
? "text-xs font-semibold leading-none" ? "text-2xs font-medium leading-3"
: "text-sm", : "text-xs font-medium leading-4",
)} )}
data-testid="message-typing-indicator-label" data-testid="message-typing-indicator-label"
> >

View File

@@ -45,7 +45,7 @@ import type { Channel, ManagedAgent, RelayAgent } from "@/shared/api/types";
import { Button } from "@/shared/ui/button"; import { Button } from "@/shared/ui/button";
import { import {
OverlayPanelBackdrop, OverlayPanelBackdrop,
PANEL_BASE_CLASS, PANEL_ENTER_BASE_CLASS,
PANEL_OVERLAY_CLASS, PANEL_OVERLAY_CLASS,
PANEL_SINGLE_COLUMN_HEADER_LAYER_CLASS, PANEL_SINGLE_COLUMN_HEADER_LAYER_CLASS,
} from "@/shared/ui/OverlayPanelBackdrop"; } from "@/shared/ui/OverlayPanelBackdrop";
@@ -469,7 +469,7 @@ export function UserProfilePanel({
{isFloatingOverlay && <OverlayPanelBackdrop onClose={onClose} />} {isFloatingOverlay && <OverlayPanelBackdrop onClose={onClose} />}
<aside <aside
className={cn( className={cn(
PANEL_BASE_CLASS, PANEL_ENTER_BASE_CLASS,
isSinglePanelView && "border-l-0", isSinglePanelView && "border-l-0",
isFloatingOverlay && PANEL_OVERLAY_CLASS, isFloatingOverlay && PANEL_OVERLAY_CLASS,
)} )}

View File

@@ -1496,7 +1496,25 @@
} }
} }
@keyframes buzz-side-panel-enter {
from {
opacity: 0;
transform: translate3d(24px, 0, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
@layer components { @layer components {
.buzz-side-panel-enter {
animation: buzz-side-panel-enter 260ms cubic-bezier(0.32, 0.72, 0, 1) both;
transform-origin: right center;
will-change: transform, opacity;
}
.buzz-huddle-shell { .buzz-huddle-shell {
--buzz-huddle-drawer-height: 5rem; --buzz-huddle-drawer-height: 5rem;
--buzz-huddle-drawer-radius: 24px; --buzz-huddle-drawer-radius: 24px;
@@ -1634,6 +1652,11 @@
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.buzz-side-panel-enter {
animation: none;
will-change: auto;
}
.buzz-huddle-app-surface { .buzz-huddle-app-surface {
transition: none; transition: none;
} }

View File

@@ -21,6 +21,10 @@ export const PANEL_BASE_CLASS =
export const PANEL_OVERLAY_CLASS = export const PANEL_OVERLAY_CLASS =
"fixed bottom-0 right-0 top-11 z-40 h-auto shadow-xl max-w-[calc(100vw-2rem)]"; "fixed bottom-0 right-0 top-11 z-40 h-auto shadow-xl max-w-[calc(100vw-2rem)]";
export const PANEL_ENTER_MOTION_CLASS = "buzz-side-panel-enter";
export const PANEL_ENTER_BASE_CLASS = `${PANEL_BASE_CLASS} ${PANEL_ENTER_MOTION_CLASS}`;
/** /**
* Single-column panel headers should render above the local panel backdrop * Single-column panel headers should render above the local panel backdrop
* (z-40) but stay below global top chrome controls (z-[45]). * (z-40) but stay below global top chrome controls (z-[45]).