feat(provider-website/PromoBanner): make the promo bar theme-driven

Replace the hardcoded dark STYLE_GRADIENT map, cream text, and fixed CTA
colors with theme tokens (background.secondary/tertiary, text.primary,
primary fill + onPrimary, themed borders/shadows). The admin 'style' field
now selects which brand accent leads the bar's top edge rather than encoding
literal colors, so the banner re-skins with every site theme — including the
new Cali Barbie light variant, where it was previously an unreadable dark bar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Natalie 2026-06-21 16:43:02 -05:00
parent 03d86597c0
commit d077629876

View file

@ -28,17 +28,28 @@ const PLATFORM_GLYPH: Record<string, string> = {
other: '🔗',
};
const STYLE_GRADIENT: Record<string, string> = {
'gradient-rose': 'linear-gradient(95deg, #2a1020 0%, #4a1530 55%, #7a2348 100%)',
'gradient-violet': 'linear-gradient(95deg, #170d2e 0%, #2c1a52 55%, #4a2a8a 100%)',
'gradient-gold': 'linear-gradient(95deg, #241a08 0%, #3d2e0f 55%, #6b5118 100%)',
'solid-dark': '#141420',
'solid-cream': 'linear-gradient(95deg, #2a2622 0%, #38322b 100%)',
/**
* Theme-driven styling: the banner background, text, CTA and borders all derive
* from the active site theme (`p.theme.*`), so the bar re-skins with every theme
* light or dark instead of shipping fixed dark gradients. The admin-set
* `style` no longer encodes literal colors; it selects which brand accent leads
* the bar's top edge (the `solid-*` styles stay neutral).
*/
type AccentKey = 'primary' | 'secondary' | 'accent' | null;
const STYLE_ACCENT: Record<string, AccentKey> = {
'gradient-rose': 'primary',
'gradient-violet': 'accent',
'gradient-gold': 'secondary',
'solid-dark': null,
'solid-cream': null,
};
function bannerBackground(banner: PromoBannerItem): string {
if (banner.creativeKind === 'image') return '#0f0f17';
return STYLE_GRADIENT[banner.style ?? 'gradient-rose'] ?? STYLE_GRADIENT['gradient-rose']!;
/** Which theme accent (if any) leads this banner's top edge. */
function bannerAccent(banner: PromoBannerItem): AccentKey {
// Image creatives carry their own look — keep the surrounding bar neutral.
if (banner.creativeKind === 'image') return null;
return STYLE_ACCENT[banner.style ?? 'gradient-rose'] ?? 'primary';
}
export function PromoBanner(): ReactNode {
@ -49,7 +60,7 @@ export function PromoBanner(): ReactNode {
const glyph = PLATFORM_GLYPH[banner.platform] ?? '🔗';
return (
<Bar role="complementary" aria-label="Promotion" style={{ background: bannerBackground(banner) }}>
<Bar role="complementary" aria-label="Promotion" $accent={bannerAccent(banner)}>
<Inner>
{banner.creativeKind === 'image' && banner.media ? (
<Thumb>
@ -93,7 +104,7 @@ const slideUp = keyframes`
to { transform: translateY(0); opacity: 1; }
`;
const Bar = styled.div`
const Bar = styled.div<{ $accent: AccentKey }>`
position: fixed;
left: 0;
right: 0;
@ -107,9 +118,17 @@ const Bar = styled.div`
padding: 0.6rem max(1rem, env(safe-area-inset-left)) calc(0.6rem + env(safe-area-inset-bottom));
/* Keep CTA + copy clear of the bottom-right floating action buttons. */
padding-right: 4.5rem;
border-top: 1px solid rgba(255, 255, 255, 0.12);
box-shadow: 0 -6px 24px rgba(0, 0, 0, 0.35);
color: #fdf6f0;
/* Themed surface bar opaque so copy stays readable on any page background.
The chosen brand accent (or the neutral border for solid styles) leads the
top edge so the bar reads as part of the active theme, light or dark. */
background: linear-gradient(
95deg,
${p => p.theme.colors.background.secondary} 0%,
${p => p.theme.colors.background.tertiary} 100%
);
color: ${p => p.theme.colors.text.primary};
border-top: 2px solid ${p => (p.$accent ? p.theme.colors[p.$accent].main : p.theme.colors.border)};
box-shadow: ${p => p.theme.shadows.lg};
@media (prefers-reduced-motion: no-preference) {
animation: ${slideUp} 360ms cubic-bezier(0.16, 1, 0.3, 1);
@ -132,7 +151,7 @@ const Thumb = styled.div`
height: 44px;
border-radius: 8px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.18);
border: 1px solid ${p => p.theme.colors.border};
img {
width: 100%;
@ -167,7 +186,7 @@ const Headline = styled.span`
const Subtext = styled.span`
font-size: 0.8rem;
opacity: 0.82;
color: ${p => p.theme.colors.text.secondary};
line-height: 1.25;
white-space: nowrap;
overflow: hidden;
@ -185,17 +204,18 @@ const Cta = styled.a`
justify-content: center;
padding: 0.5rem 1.1rem;
border-radius: 999px;
background: #fdf6f0;
color: #2a1020;
background: ${p => p.theme.colors.primary};
color: ${p => p.theme.colors.onPrimary};
font-weight: 700;
font-size: 0.875rem;
text-decoration: none;
white-space: nowrap;
box-shadow: ${p => p.theme.shadows.sm};
transition: transform 120ms ease, box-shadow 120ms ease;
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.3);
box-shadow: ${p => p.theme.shadows.md};
}
`;
@ -210,13 +230,14 @@ const CloseButton = styled.button`
width: 32px;
height: 32px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(0, 0, 0, 0.25);
color: #fdf6f0;
border: 1px solid ${p => p.theme.colors.border};
background: ${p => p.theme.colors.background.tertiary};
color: ${p => p.theme.colors.text.secondary};
cursor: pointer;
flex-shrink: 0;
&:hover {
background: rgba(0, 0, 0, 0.45);
background: ${p => p.theme.colors.background.secondary};
color: ${p => p.theme.colors.text.primary};
}
`;