CompareBar: raise above HelpBar, fix centering, responsive sizing on xs

- Raise bottom offset from theme.spacing(3) (24px) → theme.spacing(9)
  (72px) so the pill clears the sticky HelpBar with ~16px breathing.
- Centering: swap `left:50%; transform: translateX(-50%)` for
  `left:0; right:0; mx:auto; width:fit-content`. Slide (the wrapper)
  animates via transform, which was clobbering our centering transform
  and leaving the bar's left edge at the viewport centre instead of
  its centre (measured 171px off-centre pre-fix). Auto-margin
  centering doesn't fight Slide's animation.
- Mobile sizing: responsive step-down on xs —
  - Badge: large (32px) → medium (26px)
  - Typography: body1 (16px) → body2 (14px)
  - Button: medium (40px) → small (32px)
  - Container: gap 2→1.5, px 3→2, py 1.5→1
  md+ keeps the larger sizes from the earlier bump.

Rejected alternatives: slide/peek collapsed-state (adds interaction
cost and hides state behind a tap — bad for FA's grief-sensitive
audience); full-width bottom bar (loses the "floating reminder" pill
character).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 11:40:14 +10:00
parent d0462a87c8
commit 4de8a916af

View File

@@ -1,7 +1,8 @@
import React from 'react'; import React from 'react';
import Paper from '@mui/material/Paper'; import Paper from '@mui/material/Paper';
import Slide from '@mui/material/Slide'; import Slide from '@mui/material/Slide';
import type { SxProps, Theme } from '@mui/material/styles'; import useMediaQuery from '@mui/material/useMediaQuery';
import { useTheme, type SxProps, type Theme } from '@mui/material/styles';
import { Typography } from '../../atoms/Typography'; import { Typography } from '../../atoms/Typography';
import { Button } from '../../atoms/Button'; import { Button } from '../../atoms/Button';
import { Badge } from '../../atoms/Badge'; import { Badge } from '../../atoms/Badge';
@@ -42,6 +43,8 @@ export interface CompareBarProps {
*/ */
export const CompareBar = React.forwardRef<HTMLDivElement, CompareBarProps>( export const CompareBar = React.forwardRef<HTMLDivElement, CompareBarProps>(
({ packages, onCompare, error, sx }, ref) => { ({ packages, onCompare, error, sx }, ref) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const count = packages.length; const count = packages.length;
const visible = count > 0; const visible = count > 0;
const canCompare = count >= 2; const canCompare = count >= 2;
@@ -59,29 +62,42 @@ export const CompareBar = React.forwardRef<HTMLDivElement, CompareBarProps>(
sx={[ sx={[
(theme: Theme) => ({ (theme: Theme) => ({
position: 'fixed', position: 'fixed',
bottom: theme.spacing(3), // Clear the sticky HelpBar (~56px + 16px breathing) so the
left: '50%', // pill doesn't overlap it.
transform: 'translateX(-50%)', bottom: theme.spacing(9),
// Centre via auto-margin rather than `left:50%; translateX(-50%)`
// — Slide (the wrapper) animates via transform, which would
// otherwise clobber the centering transform.
left: 0,
right: 0,
mx: 'auto',
width: 'fit-content',
zIndex: theme.zIndex.snackbar, zIndex: theme.zIndex.snackbar,
borderRadius: '9999px', borderRadius: '9999px',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 2, gap: { xs: 1.5, md: 2 },
px: 3, px: { xs: 2, md: 3 },
py: 1.5, py: { xs: 1, md: 1.5 },
maxWidth: { xs: 'calc(100vw - 32px)', md: 460 }, maxWidth: { xs: 'calc(100vw - 32px)', md: 460 },
}), }),
...(Array.isArray(sx) ? sx : [sx]), ...(Array.isArray(sx) ? sx : [sx]),
]} ]}
> >
{/* Fraction badge — 1/3, 2/3, 3/3 */} {/* Fraction badge — 1/3, 2/3, 3/3. Responsive: medium on xs,
<Badge color="brand" variant="soft" size="large" sx={{ flexShrink: 0 }}> large on md+ to match the rest of the bar's size step. */}
<Badge
color="brand"
variant="soft"
size={isMobile ? 'medium' : 'large'}
sx={{ flexShrink: 0 }}
>
{count}/3 {count}/3
</Badge> </Badge>
{/* Status text */} {/* Status text — body2 on mobile (smaller footprint), body1 on md+ */}
<Typography <Typography
variant="body1" variant={isMobile ? 'body2' : 'body1'}
role={error ? 'alert' : undefined} role={error ? 'alert' : undefined}
sx={{ sx={{
fontWeight: 500, fontWeight: 500,
@@ -92,10 +108,10 @@ export const CompareBar = React.forwardRef<HTMLDivElement, CompareBarProps>(
{error || statusText} {error || statusText}
</Typography> </Typography>
{/* Compare CTA — icon dropped; label alone at medium size is enough */} {/* Compare CTA — small (32px) on mobile, medium (40px) on md+ */}
<Button <Button
variant="contained" variant="contained"
size="medium" size={isMobile ? 'small' : 'medium'}
onClick={onCompare} onClick={onCompare}
disabled={!canCompare} disabled={!canCompare}
sx={{ flexShrink: 0, borderRadius: '9999px' }} sx={{ flexShrink: 0, borderRadius: '9999px' }}