import React from 'react'; import Box from '@mui/material/Box'; import Paper from '@mui/material/Paper'; import Slide from '@mui/material/Slide'; import useMediaQuery from '@mui/material/useMediaQuery'; import IconButton from '@mui/material/IconButton'; import ChevronRightRoundedIcon from '@mui/icons-material/ChevronRightRounded'; import ChevronLeftRoundedIcon from '@mui/icons-material/ChevronLeftRounded'; import { useTheme, type SxProps, type Theme } from '@mui/material/styles'; import { Typography } from '../../atoms/Typography'; import { Button } from '../../atoms/Button'; import { Badge } from '../../atoms/Badge'; // ─── Types ─────────────────────────────────────────────────────────────────── /** A package in the comparison basket */ export interface CompareBarPackage { /** Unique package ID */ id: string; /** Package display name */ name: string; /** Provider name */ providerName: string; } /** Props for the CompareBar molecule */ export interface CompareBarProps { /** Packages currently in the comparison basket (max 3 user-selected) */ packages: CompareBarPackage[]; /** Called when user clicks "Compare" CTA */ onCompare: () => void; /** Error/status message shown inline (e.g. "Maximum 3 packages") */ error?: string; /** MUI sx prop for the root wrapper */ sx?: SxProps; } // ─── Constants ────────────────────────────────────────────────────────────── /** How long the bar stays expanded after a new package is added while * collapsed. Long enough to read, short enough not to obstruct. */ const PEEK_DURATION_MS = 3000; /** Middle-content expand/collapse duration (width + opacity). */ const COLLAPSE_MS = 300; // ─── Component ─────────────────────────────────────────────────────────────── /** * Floating comparison basket pill for the FA design system. * * Shows a fraction badge (1/3, 2/3, 3/3), contextual copy, and a Compare CTA. * Present on both ProvidersStep and PackagesStep. * * **Mobile collapse** (xs only): users can tap a right-chevron to retract * the pill to the right edge — the middle content (status text + Compare * button) animates to width:0 while the pill stays anchored at the same * right offset, so the whole thing appears to shrink into the corner as * one unit rather than two separate elements. Tap again to expand. When * a new package is added while collapsed, the bar auto-peeks for * `PEEK_DURATION_MS` so the user sees the tally update, then re-collapses. * * Desktop (md+) stays expanded — there's plenty of space, and the * collapse chevron is not rendered. * * Composes Badge + Button + Typography + IconButton. */ export const CompareBar = React.forwardRef( ({ packages, onCompare, error, sx }, ref) => { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('md')); const count = packages.length; const visible = count > 0; const canCompare = count >= 2; const statusText = count === 1 ? 'Add another to compare' : 'Ready to compare'; // Collapse state — mobile only. Starts expanded; when the basket empties // we reset so the next fresh fill starts visible. const [collapsed, setCollapsed] = React.useState(false); const [peeking, setPeeking] = React.useState(false); const lastCountRef = React.useRef(count); React.useEffect(() => { if (!visible) setCollapsed(false); }, [visible]); // Auto-peek when a package is added while collapsed. React.useEffect(() => { const prev = lastCountRef.current; lastCountRef.current = count; if (collapsed && count > prev) { setPeeking(true); const t = window.setTimeout(() => setPeeking(false), PEEK_DURATION_MS); return () => window.clearTimeout(t); } }, [count, collapsed]); /** Effective "is the middle content hidden?" — only on mobile, when the * user has collapsed and we're not currently peeking. */ const mobileCollapsed = isMobile && collapsed && !peeking; return ( ({ position: 'fixed', // Clear the sticky HelpBar (~40px) + breathing room. FA theme // uses a 4px spacing base, so spacing(16) = 64px. bottom: t.spacing(16), // z-index sits below the mobile map-view drawer (modal: 1300) // but above app chrome (appBar: 1100). snackbar (1400) was too // aggressive — the drawer visually covers this bar on mobile. zIndex: t.zIndex.drawer, // Mobile: right-anchored so when the middle collapses the pill // appears to retract to the right corner. Desktop: centered. ...(isMobile ? { right: t.spacing(4), left: 'auto' } : { left: 0, right: 0, mx: 'auto' }), width: 'fit-content', borderRadius: '9999px', display: 'flex', alignItems: 'center', gap: { xs: 1.25, md: 2 }, px: { xs: 1.5, md: 3 }, py: { xs: 0.75, md: 1.5 }, maxWidth: { xs: 'calc(100vw - 32px)', md: 460 }, overflow: 'hidden', transition: `padding ${COLLAPSE_MS}ms ease-out`, }), ...(Array.isArray(sx) ? sx : [sx]), ]} > {/* Fraction badge — shows "N/3" when expanded, just "N" when collapsed on mobile (reads as a circle at mini size). */} {mobileCollapsed ? count : `${count}/3`} {/* Middle content (status + Compare CTA) — animates to zero max-width when collapsed, letting the pill shrink as one unit with the right edge staying fixed. */} {error || statusText} {/* Mobile-only collapse/expand chevron — grey-filled circle that swaps icon direction based on state. Rendered at all times so the IconButton container stays in the layout and the icon swap happens in place without mount/unmount. */} {isMobile && ( setCollapsed((c) => !c)} size="small" sx={{ flexShrink: 0, width: 32, height: 32, borderRadius: '50%', bgcolor: 'var(--fa-color-neutral-200)', color: 'text.secondary', '&:hover': { bgcolor: 'var(--fa-color-neutral-300)' }, }} > {mobileCollapsed ? ( ) : ( )} )} ); }, ); CompareBar.displayName = 'CompareBar'; export default CompareBar;