FAB follows bottom nav, subtle shadow, fixed positioning

This commit is contained in:
Chad Curtis
2026-03-24 07:11:28 -05:00
parent 01c32a0e88
commit f30d7e3668
2 changed files with 19 additions and 13 deletions
+3 -5
View File
@@ -11,10 +11,7 @@ import { ReplyComposeModal } from '@/components/ReplyComposeModal';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { getAvatarShape, getEmojiMaskUrl } from '@/lib/avatarShape';
/** Drop shadow style for the planet FAB — hoisted to avoid re-creating on every render. */
const fabShadowStyle: React.CSSProperties = {
filter: 'drop-shadow(0 4px 12px hsl(var(--primary) / 0.4)) drop-shadow(0 2px 4px hsl(var(--primary) / 0.2))',
};
interface FloatingComposeButtonProps {
/** The Nostr event kind this FAB creates. kind=1 opens compose; others show "Coming soon". */
@@ -73,7 +70,8 @@ export function FloatingComposeButton({ kind = 1, href, onFabClick, icon }: Floa
<button
onClick={handleClick}
className="relative size-16 transition-transform hover:scale-105 active:scale-95"
style={fabShadowStyle}
style={{ filter: 'drop-shadow(0 2px 8px hsl(var(--primary) / 0.25))' }}
>
{/* FAB background: user's avatar shape (emoji mask) or circle (default) */}
<div
+16 -8
View File
@@ -10,6 +10,8 @@ import { CursorFireEffect } from '@/components/CursorFireEffect';
import { Skeleton } from '@/components/ui/skeleton';
import { LayoutStore, LayoutStoreContext, useLayoutSnapshot } from '@/contexts/LayoutContext';
import { useAppContext } from '@/hooks/useAppContext';
import { useScrollDirection } from '@/hooks/useScrollDirection';
import { ARC_UP_OVERHANG_PX } from '@/components/ArcBackground';
import { cn } from '@/lib/utils';
/** Skeleton shown in the content area while a lazy page chunk is loading. */
@@ -64,9 +66,10 @@ function PageSkeleton() {
/** Inner component that reads layout options from the context store. */
function MainLayoutInner() {
const { rightSidebar, showFAB = false, fabKind = 1, fabHref, onFabClick, fabIcon, wrapperClassName, noOverscroll, noMaxWidth } = useLayoutSnapshot();
const { rightSidebar, showFAB = false, fabKind = 1, fabHref, onFabClick, fabIcon, wrapperClassName, noOverscroll, noMaxWidth, scrollContainer } = useLayoutSnapshot();
const [drawerOpen, setDrawerOpen] = useState(false);
const { config } = useAppContext();
const { hidden: navHidden } = useScrollDirection(scrollContainer);
return (
<>
@@ -96,13 +99,6 @@ function MainLayoutInner() {
content underneath. Only active below the sidebar breakpoint. */}
<div className={cn("relative flex-1 min-w-0 sidebar:border-l sidebar:border-r border-border bg-background/85 -mt-mobile-bar", !noMaxWidth && "sidebar:max-w-[600px]", !noOverscroll && "pb-overscroll")}>
<Outlet />
{showFAB && (
<div className="sticky bottom-fab sidebar:bottom-6 z-30 pointer-events-none flex justify-end pr-6">
<div className="pointer-events-auto">
<FloatingComposeButton kind={fabKind} href={fabHref} onFabClick={onFabClick} icon={fabIcon} />
</div>
</div>
)}
</div>
{rightSidebar !== null && (rightSidebar ?? <RightSidebar />)}
</Suspense>
@@ -110,6 +106,18 @@ function MainLayoutInner() {
{/* Mobile bottom nav - only on small screens, slides out on scroll */}
<MobileBottomNav />
{/* FAB - fixed, mirrors bottom nav hide/show transition on mobile */}
{showFAB && (
<div
className="fixed bottom-fab sidebar:bottom-6 right-6 z-30 pointer-events-none sidebar:transition-none transition-transform duration-300 ease-in-out"
style={navHidden ? { transform: `translateY(calc(var(--bottom-nav-height) + env(safe-area-inset-bottom, 0px)))` } : undefined}
>
<div className="pointer-events-auto">
<FloatingComposeButton kind={fabKind} href={fabHref} onFabClick={onFabClick} icon={fabIcon} />
</div>
</div>
)}
</>
);
}