mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
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:
@@ -801,7 +801,7 @@ export function AppShell() {
|
||||
className="isolate min-h-0 min-w-0 overflow-hidden bg-sidebar"
|
||||
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
|
||||
errorMessage={channelsErrorMessage}
|
||||
/>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
@@ -22,6 +23,10 @@ type AppTopChromeProps = {
|
||||
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";
|
||||
|
||||
function preventTopChromeWheel(event: WheelEvent) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function TopChromeSidebarTrigger() {
|
||||
const sidebar = useOptionalSidebar();
|
||||
|
||||
@@ -50,6 +55,7 @@ export function AppTopChrome({
|
||||
onGoBack,
|
||||
onGoForward,
|
||||
}: AppTopChromeProps) {
|
||||
const topChromeRef = React.useRef<HTMLDivElement>(null);
|
||||
const isFullscreen = useIsFullscreen();
|
||||
// On macOS the traffic-light buttons overlay the chrome (see
|
||||
// `trafficLightPosition` in `tauri.conf.json`), so the nav row clears their
|
||||
@@ -60,14 +66,29 @@ export function AppTopChrome({
|
||||
const navRowAlignmentClass =
|
||||
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 (
|
||||
<div
|
||||
ref={topChromeRef}
|
||||
className={cn(
|
||||
"relative z-45 flex shrink-0 cursor-default select-none items-center bg-sidebar pr-3 text-sidebar-foreground",
|
||||
topChromeBackdrop.height,
|
||||
navRowPaddingClass,
|
||||
)}
|
||||
data-tauri-drag-region
|
||||
data-testid="app-top-chrome"
|
||||
>
|
||||
<div className={cn("flex items-center gap-0.5", navRowAlignmentClass)}>
|
||||
<TopChromeSidebarTrigger />
|
||||
|
||||
@@ -20,7 +20,7 @@ import { Button } from "@/shared/ui/button";
|
||||
import type { UserProfileLookup } from "@/features/profile/lib/identity";
|
||||
import {
|
||||
OverlayPanelBackdrop,
|
||||
PANEL_BASE_CLASS,
|
||||
PANEL_ENTER_BASE_CLASS,
|
||||
PANEL_OVERLAY_CLASS,
|
||||
PANEL_SINGLE_COLUMN_HEADER_LAYER_CLASS,
|
||||
} from "@/shared/ui/OverlayPanelBackdrop";
|
||||
@@ -179,7 +179,7 @@ export function AgentSessionThreadPanel({
|
||||
{isFloatingOverlay && <OverlayPanelBackdrop onClose={onClose} />}
|
||||
<aside
|
||||
className={cn(
|
||||
PANEL_BASE_CLASS,
|
||||
PANEL_ENTER_BASE_CLASS,
|
||||
isSinglePanelView && "border-l-0",
|
||||
isFloatingOverlay && PANEL_OVERLAY_CLASS,
|
||||
)}
|
||||
|
||||
@@ -33,6 +33,7 @@ export function ChannelManagementAuxiliaryPanel({
|
||||
}: ChannelManagementAuxiliaryPanelProps) {
|
||||
const panel = (
|
||||
<ChannelManagementSheet
|
||||
animateSplitEnter={isSinglePanelView && !useSplitAuxiliaryPane}
|
||||
channel={activeChannel}
|
||||
currentPubkey={currentPubkey}
|
||||
layout={useSplitAuxiliaryPane || isSinglePanelView ? "split" : "overlay"}
|
||||
|
||||
@@ -62,6 +62,7 @@ import { useScrollBoundaryLock } from "@/shared/hooks/useScrollBoundaryLock";
|
||||
import {
|
||||
OverlayPanelBackdrop,
|
||||
PANEL_BASE_CLASS,
|
||||
PANEL_ENTER_MOTION_CLASS,
|
||||
PANEL_OVERLAY_CLASS,
|
||||
} from "@/shared/ui/OverlayPanelBackdrop";
|
||||
import { ChannelCanvas } from "./ChannelCanvas";
|
||||
@@ -81,6 +82,7 @@ import { ChannelManagementModerationActions } from "./ChannelManagementModeratio
|
||||
|
||||
type ChannelManagementSheetProps = {
|
||||
channel: Channel | null;
|
||||
animateSplitEnter?: boolean;
|
||||
currentPubkey?: string;
|
||||
layout?: "overlay" | "split";
|
||||
onDeleted?: () => void;
|
||||
@@ -91,6 +93,7 @@ type ChannelManagementSheetProps = {
|
||||
const DEFAULT_EPHEMERAL_TTL_SECONDS = 7 * 24 * 60 * 60;
|
||||
|
||||
export function ChannelManagementSheet({
|
||||
animateSplitEnter = false,
|
||||
channel,
|
||||
currentPubkey,
|
||||
layout = "overlay",
|
||||
@@ -315,6 +318,7 @@ export function ChannelManagementSheet({
|
||||
className={cn(
|
||||
PANEL_BASE_CLASS,
|
||||
"h-full w-full cursor-default overflow-hidden border-l-0 p-0",
|
||||
animateSplitEnter && PANEL_ENTER_MOTION_CLASS,
|
||||
isDark
|
||||
? "bg-background/85 backdrop-blur-xl supports-[backdrop-filter]:bg-background/75"
|
||||
: "bg-background",
|
||||
@@ -361,7 +365,8 @@ export function ChannelManagementSheet({
|
||||
className={cn(
|
||||
PANEL_BASE_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
|
||||
? "bg-background/85 backdrop-blur-xl supports-[backdrop-filter]:bg-background/75"
|
||||
: "bg-background",
|
||||
|
||||
@@ -794,7 +794,7 @@ export const ChannelPane = React.memo(function ChannelPane({
|
||||
{hasTypingActivity ? (
|
||||
<TypingIndicatorRow
|
||||
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}
|
||||
profiles={profiles}
|
||||
typingPubkeys={typingPubkeys}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type * as React from "react";
|
||||
|
||||
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 = {
|
||||
canResetWidth: boolean;
|
||||
@@ -23,7 +25,10 @@ export function RightAuxiliaryPane({
|
||||
}: RightAuxiliaryPaneProps) {
|
||||
return (
|
||||
<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}
|
||||
style={{
|
||||
maxWidth: constrainToAvailableSpace
|
||||
|
||||
@@ -104,7 +104,7 @@ export const MessageComposerToolbar = React.memo(
|
||||
editor={editor}
|
||||
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
|
||||
* are popped out of flow immediately so entering elements
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
OverlayPanelBackdrop,
|
||||
PANEL_BASE_CLASS,
|
||||
PANEL_ENTER_BASE_CLASS,
|
||||
PANEL_OVERLAY_CLASS,
|
||||
PANEL_SINGLE_COLUMN_HEADER_LAYER_CLASS,
|
||||
} from "@/shared/ui/OverlayPanelBackdrop";
|
||||
@@ -207,7 +207,7 @@ function ThreadComposerSkeleton() {
|
||||
</div>
|
||||
<div
|
||||
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,
|
||||
)}
|
||||
/>
|
||||
@@ -304,7 +304,7 @@ export function MessageThreadPanelSkeleton({
|
||||
{isFloatingOverlay && <OverlayPanelBackdrop onClose={onClose} />}
|
||||
<aside
|
||||
className={cn(
|
||||
PANEL_BASE_CLASS,
|
||||
PANEL_ENTER_BASE_CLASS,
|
||||
isSinglePanelView && "border-l-0",
|
||||
isFloatingOverlay && PANEL_OVERLAY_CLASS,
|
||||
)}
|
||||
@@ -902,7 +902,7 @@ export function MessageThreadPanel({
|
||||
/>
|
||||
<div
|
||||
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,
|
||||
)}
|
||||
>
|
||||
@@ -913,7 +913,7 @@ export function MessageThreadPanel({
|
||||
{threadTypingPubkeys.length > 0 ? (
|
||||
<TypingIndicatorRow
|
||||
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}
|
||||
profiles={profiles}
|
||||
typingPubkeys={threadTypingPubkeys}
|
||||
@@ -974,7 +974,7 @@ export function MessageThreadPanel({
|
||||
{isFloatingOverlay && <OverlayPanelBackdrop onClose={onClose} />}
|
||||
<aside
|
||||
className={cn(
|
||||
PANEL_BASE_CLASS,
|
||||
PANEL_ENTER_BASE_CLASS,
|
||||
isSinglePanelView && "border-l-0",
|
||||
isFloatingOverlay && PANEL_OVERLAY_CLASS,
|
||||
)}
|
||||
|
||||
@@ -90,8 +90,8 @@ export function TypingIndicatorRow({
|
||||
{labels.length > 0 && (
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full items-center gap-2",
|
||||
isActivityVariant && "h-full",
|
||||
"flex min-w-0 w-full items-center",
|
||||
isActivityVariant ? "h-full gap-1.5" : "gap-2",
|
||||
)}
|
||||
>
|
||||
<div className="flex shrink-0 items-center">
|
||||
@@ -103,7 +103,7 @@ export function TypingIndicatorRow({
|
||||
key={pubkey}
|
||||
className={cn(
|
||||
"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",
|
||||
)}
|
||||
data-testid="message-typing-avatar"
|
||||
@@ -113,7 +113,7 @@ export function TypingIndicatorRow({
|
||||
label={label}
|
||||
className={cn(
|
||||
isActivityVariant
|
||||
? "h-[18px] w-[18px] text-3xs"
|
||||
? "h-4 w-4 text-3xs"
|
||||
: "h-5 w-5 text-3xs",
|
||||
)}
|
||||
iconClassName={
|
||||
@@ -126,10 +126,10 @@ export function TypingIndicatorRow({
|
||||
</div>
|
||||
<p
|
||||
className={cn(
|
||||
"truncate text-muted-foreground",
|
||||
"min-w-0 translate-y-px truncate text-muted-foreground",
|
||||
isActivityVariant
|
||||
? "text-xs font-semibold leading-none"
|
||||
: "text-sm",
|
||||
? "text-2xs font-medium leading-3"
|
||||
: "text-xs font-medium leading-4",
|
||||
)}
|
||||
data-testid="message-typing-indicator-label"
|
||||
>
|
||||
|
||||
@@ -45,7 +45,7 @@ import type { Channel, ManagedAgent, RelayAgent } from "@/shared/api/types";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
OverlayPanelBackdrop,
|
||||
PANEL_BASE_CLASS,
|
||||
PANEL_ENTER_BASE_CLASS,
|
||||
PANEL_OVERLAY_CLASS,
|
||||
PANEL_SINGLE_COLUMN_HEADER_LAYER_CLASS,
|
||||
} from "@/shared/ui/OverlayPanelBackdrop";
|
||||
@@ -469,7 +469,7 @@ export function UserProfilePanel({
|
||||
{isFloatingOverlay && <OverlayPanelBackdrop onClose={onClose} />}
|
||||
<aside
|
||||
className={cn(
|
||||
PANEL_BASE_CLASS,
|
||||
PANEL_ENTER_BASE_CLASS,
|
||||
isSinglePanelView && "border-l-0",
|
||||
isFloatingOverlay && PANEL_OVERLAY_CLASS,
|
||||
)}
|
||||
|
||||
@@ -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 {
|
||||
.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-drawer-height: 5rem;
|
||||
--buzz-huddle-drawer-radius: 24px;
|
||||
@@ -1634,6 +1652,11 @@
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.buzz-side-panel-enter {
|
||||
animation: none;
|
||||
will-change: auto;
|
||||
}
|
||||
|
||||
.buzz-huddle-app-surface {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,10 @@ export const PANEL_BASE_CLASS =
|
||||
export const PANEL_OVERLAY_CLASS =
|
||||
"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
|
||||
* (z-40) but stay below global top chrome controls (z-[45]).
|
||||
|
||||
Reference in New Issue
Block a user