Refine ProviderCard v2 — logo, price, badges, footer, unverified treatment
- Logo: circle → 64px rounded rectangle (8px radius), positioned fully inside image area with white border + shadow - Footer removed — redundant since whole card is clickable and price is already in content area - Price: split "Packages from" (body2) + price (h6/500wt) for lighter ecommerce feel, replaces blocky labelLg/700 - Verified badge bumped to medium size for visibility - Capability badge: medium size, trailing info icon + capabilityDescription tooltip prop for plain-language definitions on hover - Unverified cards: 3px top accent bar, list on neutral.50 background - Caption/CaptionSm weight: 400 → 500 system-wide (extends D019) - Meta row: body2 → caption size for clearer tertiary hierarchy Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import type { SxProps, Theme } from '@mui/material/styles';
|
||||
import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined';
|
||||
import StarRoundedIcon from '@mui/icons-material/StarRounded';
|
||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||
import VerifiedOutlinedIcon from '@mui/icons-material/VerifiedOutlined';
|
||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||
import { Card } from '../../atoms/Card';
|
||||
import { Badge } from '../../atoms/Badge';
|
||||
import type { BadgeColor } from '../../atoms/Badge/Badge';
|
||||
@@ -22,7 +23,7 @@ export interface ProviderCardProps {
|
||||
verified?: boolean;
|
||||
/** Hero image URL — only rendered when verified */
|
||||
imageUrl?: string;
|
||||
/** Provider logo URL — circular overlay on image, only rendered when verified */
|
||||
/** Provider logo URL — rounded rectangle overlay on image, only rendered when verified */
|
||||
logoUrl?: string;
|
||||
/** Average rating (e.g. 4.8). Omit to hide reviews. */
|
||||
rating?: number;
|
||||
@@ -32,7 +33,9 @@ export interface ProviderCardProps {
|
||||
capabilityLabel?: string;
|
||||
/** Capability badge colour intent — maps to Badge colour */
|
||||
capabilityColor?: BadgeColor;
|
||||
/** Starting price in dollars (shown in footer as "Packages from $X") */
|
||||
/** Tooltip description for the capability badge (shown on hover/focus) */
|
||||
capabilityDescription?: string;
|
||||
/** Starting price in dollars (shown as "From $X") */
|
||||
startingPrice?: number;
|
||||
/** Click handler — entire card is clickable */
|
||||
onClick?: () => void;
|
||||
@@ -43,13 +46,10 @@ export interface ProviderCardProps {
|
||||
// ─── Constants ───────────────────────────────────────────────────────────────
|
||||
|
||||
const LOGO_SIZE = 'var(--fa-provider-card-logo-size)';
|
||||
const LOGO_OVERLAP = 28; // half of 56px logo, in px
|
||||
const LOGO_BORDER_RADIUS = 'var(--fa-provider-card-logo-border-radius)';
|
||||
const IMAGE_HEIGHT = 'var(--fa-provider-card-image-height)';
|
||||
const CONTENT_PADDING = 'var(--fa-provider-card-content-padding)';
|
||||
const CONTENT_GAP = 'var(--fa-provider-card-content-gap)';
|
||||
const FOOTER_BG = 'var(--fa-provider-card-footer-background)';
|
||||
const FOOTER_PX = 'var(--fa-provider-card-footer-padding-x)';
|
||||
const FOOTER_PY = 'var(--fa-provider-card-footer-padding-y)';
|
||||
|
||||
// ─── Component ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -60,9 +60,9 @@ const FOOTER_PY = 'var(--fa-provider-card-footer-padding-y)';
|
||||
* list. Supports verified (paid partner) and unverified (scraped listing)
|
||||
* providers with consistent text alignment for scan readability.
|
||||
*
|
||||
* **Verified providers** get a hero image, logo overlay, and "Verified"
|
||||
* badge. **Unverified providers** show text content only — no image, logo,
|
||||
* or verification badge.
|
||||
* **Verified providers** get a hero image, logo (rounded rectangle inside
|
||||
* image area), and "Verified" badge. **Unverified providers** show text
|
||||
* content only with a subtle top accent bar for visibility in mixed lists.
|
||||
*
|
||||
* Composes: Card (interactive, padding="none"), Badge, Typography.
|
||||
*
|
||||
@@ -95,6 +95,7 @@ export const ProviderCard = React.forwardRef<HTMLDivElement, ProviderCardProps>(
|
||||
reviewCount,
|
||||
capabilityLabel,
|
||||
capabilityColor = 'default',
|
||||
capabilityDescription,
|
||||
startingPrice,
|
||||
onClick,
|
||||
sx,
|
||||
@@ -118,6 +119,11 @@ export const ProviderCard = React.forwardRef<HTMLDivElement, ProviderCardProps>(
|
||||
'&:hover': {
|
||||
backgroundColor: 'background.paper',
|
||||
},
|
||||
// Unverified cards: subtle top accent so they don't get lost
|
||||
// in a mixed list. Verified cards have the hero image as anchor.
|
||||
...(!showImage && {
|
||||
borderTop: '3px solid var(--fa-color-border-default)',
|
||||
}),
|
||||
},
|
||||
...(Array.isArray(sx) ? sx : [sx]),
|
||||
]}
|
||||
@@ -139,14 +145,14 @@ export const ProviderCard = React.forwardRef<HTMLDivElement, ProviderCardProps>(
|
||||
<Badge
|
||||
variant="filled"
|
||||
color="brand"
|
||||
size="small"
|
||||
size="medium"
|
||||
icon={<VerifiedOutlinedIcon />}
|
||||
>
|
||||
Verified
|
||||
</Badge>
|
||||
</Box>
|
||||
|
||||
{/* Logo overlay */}
|
||||
{/* Logo — fully inside image area, bottom-left */}
|
||||
{showLogo && (
|
||||
<Box
|
||||
component="img"
|
||||
@@ -154,14 +160,15 @@ export const ProviderCard = React.forwardRef<HTMLDivElement, ProviderCardProps>(
|
||||
alt={`${name} logo`}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
bottom: -LOGO_OVERLAP,
|
||||
bottom: 12,
|
||||
left: CONTENT_PADDING,
|
||||
width: LOGO_SIZE,
|
||||
height: LOGO_SIZE,
|
||||
borderRadius: '50%',
|
||||
borderRadius: LOGO_BORDER_RADIUS,
|
||||
objectFit: 'cover',
|
||||
backgroundColor: 'background.paper',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.12)',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
|
||||
border: '2px solid white',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -175,20 +182,28 @@ export const ProviderCard = React.forwardRef<HTMLDivElement, ProviderCardProps>(
|
||||
flexDirection: 'column',
|
||||
gap: CONTENT_GAP,
|
||||
p: CONTENT_PADDING,
|
||||
// Extra top padding when logo overlaps into content
|
||||
...(showLogo && { pt: `calc(${CONTENT_PADDING} + ${LOGO_OVERLAP}px)` }),
|
||||
}}
|
||||
>
|
||||
{/* Provider name */}
|
||||
{/* Provider name — full width, no logo competition */}
|
||||
<Typography variant="h5" maxLines={2}>
|
||||
{name}
|
||||
</Typography>
|
||||
|
||||
{/* Price — primary comparison data, prominent position */}
|
||||
{/* Price — "Packages from $X" with subtle size differentiation */}
|
||||
{startingPrice != null && (
|
||||
<Typography variant="labelLg" sx={{ fontWeight: 700 }} color="primary">
|
||||
From ${startingPrice.toLocaleString('en-AU')}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'baseline', gap: 0.5 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Packages from
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h6"
|
||||
component="span"
|
||||
color="primary"
|
||||
sx={{ fontWeight: 500, letterSpacing: '-0.01em' }}
|
||||
>
|
||||
${startingPrice.toLocaleString('en-AU')}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Meta row: location + reviews */}
|
||||
@@ -203,9 +218,9 @@ export const ProviderCard = React.forwardRef<HTMLDivElement, ProviderCardProps>(
|
||||
{/* Location */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<LocationOnOutlinedIcon
|
||||
sx={{ fontSize: 16, color: 'text.secondary' }}
|
||||
sx={{ fontSize: 14, color: 'text.secondary' }}
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{location}
|
||||
</Typography>
|
||||
</Box>
|
||||
@@ -217,10 +232,10 @@ export const ProviderCard = React.forwardRef<HTMLDivElement, ProviderCardProps>(
|
||||
aria-label={`Rated ${rating} out of 5${reviewCount != null ? `, ${reviewCount} reviews` : ''}`}
|
||||
>
|
||||
<StarRoundedIcon
|
||||
sx={{ fontSize: 16, color: 'warning.main' }}
|
||||
sx={{ fontSize: 14, color: 'warning.main' }}
|
||||
aria-hidden
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{rating}
|
||||
{reviewCount != null && ` (${reviewCount.toLocaleString('en-AU')})`}
|
||||
</Typography>
|
||||
@@ -228,35 +243,34 @@ export const ProviderCard = React.forwardRef<HTMLDivElement, ProviderCardProps>(
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Capability badge */}
|
||||
{/* Capability badge — trailing info icon signals hover-for-definition */}
|
||||
{capabilityLabel && (
|
||||
<Box>
|
||||
<Badge color={capabilityColor} size="small">
|
||||
{capabilityLabel}
|
||||
</Badge>
|
||||
{capabilityDescription ? (
|
||||
<Tooltip
|
||||
title={capabilityDescription}
|
||||
arrow
|
||||
placement="top"
|
||||
enterTouchDelay={0}
|
||||
>
|
||||
<Badge
|
||||
color={capabilityColor}
|
||||
size="medium"
|
||||
sx={{ cursor: 'help' }}
|
||||
>
|
||||
{capabilityLabel}
|
||||
<InfoOutlinedIcon />
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Badge color={capabilityColor} size="medium">
|
||||
{capabilityLabel}
|
||||
<InfoOutlinedIcon />
|
||||
</Badge>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* ── Footer bar ── */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
backgroundColor: FOOTER_BG,
|
||||
px: FOOTER_PX,
|
||||
py: FOOTER_PY,
|
||||
}}
|
||||
>
|
||||
<Typography variant="label" color="text.secondary">
|
||||
View packages
|
||||
</Typography>
|
||||
<ChevronRightIcon
|
||||
sx={{ fontSize: 20, color: 'text.secondary' }}
|
||||
aria-hidden
|
||||
/>
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user