diff --git a/packages/shared/src/components/PromotionalBanner.tsx b/packages/shared/src/components/PromotionalBanner.tsx index 34d7da15e8..4d50f4b8b6 100644 --- a/packages/shared/src/components/PromotionalBanner.tsx +++ b/packages/shared/src/components/PromotionalBanner.tsx @@ -15,36 +15,103 @@ import { useViewSize, ViewSize } from '../hooks/useViewSize'; type Ink = 'white' | 'pepper' | 'invert' | 'theme'; -const inkClassNames: Record = { - white: { text: 'text-white', cta: 'btn-on-fill-solid-white' }, - pepper: { text: 'text-raw-pepper-90', cta: 'btn-on-fill-solid-pepper' }, - invert: { text: 'text-surface-invert', cta: 'btn-on-fill-solid-invert' }, - theme: { text: 'text-text-primary' }, +const inkClassNames: Record = { + white: 'text-white', + pepper: 'text-raw-pepper-90', + invert: 'text-surface-invert', + theme: 'text-text-primary', }; -const stylesByTheme: Record = { +// `theme`: the app theme's primary. `white`: always the white primary, for +// fills whose text is white in both modes. `inverted`: the opposite theme's +// primary, for the neutral fills that flip with the theme. +type Cta = 'theme' | 'white' | 'inverted'; + +const ctaClassNames: Record = { + theme: undefined, + white: 'btn-primary-white', + inverted: 'btn-primary-inverted', +}; + +type ThemeStyle = { fill: string; ink: Ink; cta: Cta }; + +const stylesByTheme: Record = { [BannerCustomTheme.CabbageOnion]: { fill: 'from-accent-cabbage-subtler to-accent-onion-subtler bg-gradient-to-r', ink: 'invert', + cta: 'white', }, [BannerCustomTheme.WhitePepper]: { fill: 'bg-surface-primary', ink: 'invert', + cta: 'inverted', + }, + [Theme.Avocado]: { + fill: 'bg-accent-avocado-default', + ink: 'pepper', + cta: 'theme', + }, + [Theme.Bacon]: { + fill: 'bg-accent-bacon-default', + ink: 'pepper', + cta: 'theme', + }, + [Theme.BlueCheese]: { + fill: 'bg-accent-blueCheese-default', + ink: 'pepper', + cta: 'theme', + }, + [Theme.Bun]: { fill: 'bg-accent-bun-default', ink: 'pepper', cta: 'theme' }, + [Theme.Burger]: { + fill: 'bg-accent-burger-default', + ink: 'white', + cta: 'white', + }, + [Theme.Cabbage]: { + fill: 'bg-accent-cabbage-default', + ink: 'invert', + cta: 'white', + }, + [Theme.Cheese]: { + fill: 'bg-accent-cheese-default', + ink: 'pepper', + cta: 'theme', + }, + [Theme.Ketchup]: { + fill: 'bg-accent-ketchup-default', + ink: 'invert', + cta: 'white', + }, + [Theme.Lettuce]: { + fill: 'bg-accent-lettuce-default', + ink: 'pepper', + cta: 'theme', + }, + [Theme.Onion]: { + fill: 'bg-accent-onion-default', + ink: 'white', + cta: 'white', + }, + [Theme.Water]: { + fill: 'bg-accent-water-default', + ink: 'invert', + cta: 'white', + }, + [Theme.Salt]: { + fill: 'bg-accent-salt-default', + ink: 'invert', + cta: 'inverted', + }, + [Theme.Pepper]: { + fill: 'bg-accent-pepper-default', + ink: 'theme', + cta: 'theme', + }, + [Theme.Background]: { + fill: 'bg-background-default', + ink: 'theme', + cta: 'theme', }, - [Theme.Avocado]: { fill: 'bg-accent-avocado-default', ink: 'pepper' }, - [Theme.Bacon]: { fill: 'bg-accent-bacon-default', ink: 'pepper' }, - [Theme.BlueCheese]: { fill: 'bg-accent-blueCheese-default', ink: 'pepper' }, - [Theme.Bun]: { fill: 'bg-accent-bun-default', ink: 'pepper' }, - [Theme.Burger]: { fill: 'bg-accent-burger-default', ink: 'white' }, - [Theme.Cabbage]: { fill: 'bg-accent-cabbage-default', ink: 'invert' }, - [Theme.Cheese]: { fill: 'bg-accent-cheese-default', ink: 'pepper' }, - [Theme.Ketchup]: { fill: 'bg-accent-ketchup-default', ink: 'invert' }, - [Theme.Lettuce]: { fill: 'bg-accent-lettuce-default', ink: 'pepper' }, - [Theme.Onion]: { fill: 'bg-accent-onion-default', ink: 'white' }, - [Theme.Water]: { fill: 'bg-accent-water-default', ink: 'invert' }, - [Theme.Salt]: { fill: 'bg-accent-salt-default', ink: 'invert' }, - [Theme.Pepper]: { fill: 'bg-accent-pepper-default', ink: 'theme' }, - [Theme.Background]: { fill: 'bg-background-default', ink: 'theme' }, }; export type PromotionalBannerViewProps = { @@ -60,10 +127,9 @@ export function PromotionalBannerView({ onDismiss, className, }: PromotionalBannerViewProps): ReactElement { - const { fill, ink } = + const { fill, ink, cta } = stylesByTheme[banner.theme] ?? stylesByTheme[BannerCustomTheme.CabbageOnion]; - const { text, cta } = inkClassNames[ink]; const isLaptop = useViewSize(ViewSize.Laptop); const buttonSize = isLaptop ? ButtonSize.XSmall : ButtonSize.Small; @@ -72,7 +138,7 @@ export function PromotionalBannerView({ className={classNames( 'relative z-3 flex w-full flex-col items-start py-3 pl-3 pr-12 typo-footnote tablet:pl-20 laptop:fixed laptop:h-8 laptop:flex-row laptop:items-center laptop:justify-center laptop:px-10 laptop:py-0', fill, - text, + inkClassNames[ink], className, )} > @@ -84,7 +150,10 @@ export function PromotionalBannerView({ href={banner.url} size={buttonSize} variant={ButtonVariant.Primary} - className={classNames('mt-2 laptop:ml-4 laptop:mt-0', cta)} + className={classNames( + 'mt-2 laptop:ml-4 laptop:mt-0', + ctaClassNames[cta], + )} onClick={onCtaClick} > {banner.cta} diff --git a/packages/shared/src/styles/components/buttons.css b/packages/shared/src/styles/components/buttons.css index 467e478fb9..42e9ae960e 100644 --- a/packages/shared/src/styles/components/buttons.css +++ b/packages/shared/src/styles/components/buttons.css @@ -157,29 +157,24 @@ } /* - * Solid CTA on a brand fill: the neutral primary of the theme the fill's - * ink implies, so a pale bar gets the dark button and a deep bar the - * white one whatever the app theme. `-invert` is for fills painted with - * `bg-surface-primary`, which flip with the theme. + * The neutral primary pinned to its white form, for a brand fill whose + * text is white in both modes so the button stays on the same ink. */ - &.btn-on-fill-solid-white, - .invert &.btn-on-fill-solid-white { + &.btn-primary-white, + .invert &.btn-primary-white { --button-default-color: var(--btn-label-on-fill-dark); --button-default-background: theme('colors.white'); --button-hover-background: theme('colors.raw.salt.30'); --button-active-background: theme('colors.raw.salt.50'); } - &.btn-on-fill-solid-pepper, - .invert &.btn-on-fill-solid-pepper { - --button-default-color: var(--btn-label-on-fill-light); - --button-default-background: theme('colors.raw.pepper.90'); - --button-hover-background: theme('colors.raw.pepper.70'); - --button-active-background: theme('colors.raw.pepper.50'); - } - - &.btn-on-fill-solid-invert, - .invert &.btn-on-fill-solid-invert { + /* + * The neutral primary of the opposite theme, for a fill painted with + * `bg-surface-primary` (white in dark mode, dark in light mode) where the + * theme's own primary would match the fill. + */ + &.btn-primary-inverted, + .invert &.btn-primary-inverted { --button-default-color: var(--theme-surface-primary); --button-default-background: var(--theme-surface-invert); --button-hover-background: color-mix(in srgb, var(--theme-surface-invert), var(--theme-surface-primary) 12%); diff --git a/packages/storybook/stories/components/AnnouncementBar.stories.tsx b/packages/storybook/stories/components/AnnouncementBar.stories.tsx index c5d3702c06..303746f812 100644 --- a/packages/storybook/stories/components/AnnouncementBar.stories.tsx +++ b/packages/storybook/stories/components/AnnouncementBar.stories.tsx @@ -40,75 +40,75 @@ const themes: ThemeEntry[] = [ { theme: BannerCustomTheme.CabbageOnion, label: 'cabbage-onion (default)', - note: 'Moved from the -default to the -subtler shades with flipping ink: dark text on a lighter gradient in dark theme, white on a deeper one in light theme. At -default no ink passed AA at both ends in dark theme (white 3.8 at the cabbage end, dark 3.6 at the onion end).', + note: 'Moved from the -default to the -subtler shades with flipping ink: dark text on a lighter gradient in dark theme, white on a deeper one in light theme. At -default no ink passed AA at both ends in dark theme. White button in both modes (6.3 against the cabbage end in light mode).', }, { theme: BannerCustomTheme.WhitePepper, label: 'white-pepper', - note: 'CTA is now the neutral primary of the opposite theme: dark button on the white bar, white button on the dark one. Was the cabbage brand button.', + note: 'CTA is the neutral primary of the opposite theme: dark button on the white bar in dark mode, white button on the dark bar in light mode. Was the cabbage brand button.', }, { theme: Theme.Avocado, label: 'avocado', - note: 'Ink unchanged. CTA is now always the dark button; the white one had 1.7:1 against the fill in dark theme.', + note: 'Ink unchanged. CTA follows the app theme: white in dark mode, dark in light mode (white would be 2.0 against the fill there).', }, { theme: Theme.Bacon, label: 'bacon', - note: 'Proposed: dark ink. White text was 3.2:1 in dark theme; the button system already puts a dark label on bacon fills.', + note: 'Proposed: dark ink. White text was 3.2:1 in dark theme; the button system already puts a dark label on bacon fills. CTA follows the app theme, matching the dark text in light mode.', }, { theme: Theme.BlueCheese, label: 'blue-cheese', - note: 'Ink unchanged. CTA is now always the dark button (was 1.7:1 in dark theme).', + note: 'Ink unchanged. CTA follows the app theme (white would be 2.1 in light mode).', }, { theme: Theme.Bun, label: 'bun', - note: 'Proposed: dark ink. White text was 2.2:1 in dark theme, the worst pair on the list; the button system already puts a dark label on bun fills.', + note: 'Proposed: dark ink. White text was 2.2:1 in dark theme, the worst pair on the list; the button system already puts a dark label on bun fills. CTA follows the app theme (white would be 2.9 in light mode).', }, { theme: Theme.Burger, label: 'burger', - note: 'Unchanged. White passes in both themes (4.6 / 6.3); dark ink would drop to 4.1 in dark theme.', + note: 'Unchanged ink: white passes in both themes (4.6 / 6.3). White button in both modes.', }, { theme: Theme.Cabbage, label: 'cabbage', - note: 'Flipping ink: dark text in dark theme, white in light. White text was 3.8:1 in dark theme and no dark-theme shade of cabbage reaches 4.5 with white; dark text on -default does (4.9).', + note: 'Flipping ink: dark text in dark theme, white in light. White text was 3.8:1 in dark theme and no dark-theme shade of cabbage reaches 4.5 with white; dark text on -default does (4.9). White button in both modes (4.9 in light).', }, { theme: Theme.Cheese, label: 'cheese', - note: 'Ink unchanged. CTA is now always the dark button (was 1.3:1 in dark theme).', + note: 'Ink unchanged. CTA follows the app theme (white would be 1.4 in light mode).', }, { theme: Theme.Ketchup, label: 'ketchup', - note: 'Flipping ink, same reasoning as cabbage: white text was 3.9:1 in dark theme; dark text on -default is 4.8.', + note: 'Flipping ink, same reasoning as cabbage: white text was 3.9:1 in dark theme; dark text on -default is 4.8. White button in both modes (5.1 in light).', }, { theme: Theme.Lettuce, label: 'lettuce', - note: 'Ink unchanged. CTA is now always the dark button (was 1.3:1 in dark theme).', + note: 'Ink unchanged. CTA follows the app theme (white would be 1.5 in light mode).', }, { theme: Theme.Onion, label: 'onion', isNew: true, - note: 'New. White ink passes in both themes (5.2 / 7.1), the strongest of the saturated set.', + note: 'New. White ink passes in both themes (5.2 / 7.1), the strongest of the saturated set. White button in both modes.', }, { theme: Theme.Water, label: 'water', isNew: true, - note: 'New. Flipping ink like cabbage: white would be 3.8:1 in dark theme, dark text on -default is 4.9; light theme keeps white at 5.1.', + note: 'New. Flipping ink like cabbage: white would be 3.8:1 in dark theme, dark text on -default is 4.9; light theme keeps white at 5.1. White button in both modes.', }, { theme: Theme.Salt, label: 'salt', isNew: true, - note: 'New neutral that stands out from the app: light gray in dark theme, dark gray in light theme, like white-pepper but softer.', + note: 'New neutral that stands out from the app: light gray in dark theme, dark gray in light theme, like white-pepper but softer, and the CTA inverts the same way.', }, { theme: Theme.Pepper, @@ -314,8 +314,13 @@ const contrast = (a: string, b: string): number => { type Swatch = { fills: string[]; ink: string; cta: string }; // What each theme resolves to per app theme: bar fill, text ink, CTA fill. +// The CTA is the theme's primary (white in dark, dark in light) on dark-ink +// fills, always white on white-ink fills, and inverted on the neutral fills +// that flip with the theme. const resolve = (theme: BannerTheme, mode: CanvasTheme): Swatch => { const shade = mode === 'dark' ? '40' : '60'; + const themeCta = mode === 'dark' ? WHITE : PEPPER; + const invertedCta = mode === 'dark' ? PEPPER : WHITE; const white = (name: string): Swatch => ({ fills: [palette[name][shade]], ink: WHITE, @@ -324,36 +329,35 @@ const resolve = (theme: BannerTheme, mode: CanvasTheme): Swatch => { const pepper = (name: string): Swatch => ({ fills: [palette[name][shade]], ink: PEPPER, - cta: PEPPER, + cta: themeCta, }); // Flipping ink: dark text in dark theme, white in light theme. - const invert = (fills: string[]): Swatch => - mode === 'dark' - ? { fills, ink: PEPPER, cta: PEPPER } - : { fills, ink: WHITE, cta: WHITE }; + const invert = (fills: string[], cta = WHITE): Swatch => + mode === 'dark' ? { fills, ink: PEPPER, cta } : { fills, ink: WHITE, cta }; switch (theme) { case BannerCustomTheme.CabbageOnion: { const subtler = mode === 'dark' ? '20' : '80'; return invert([palette.cabbage[subtler], palette.onion[subtler]]); } case BannerCustomTheme.WhitePepper: - return invert([mode === 'dark' ? WHITE : PEPPER]); + return invert([mode === 'dark' ? WHITE : PEPPER], invertedCta); case Theme.Salt: - return invert([ - mode === 'dark' ? palette.salt['90'] : palette.pepper['10'], - ]); + return invert( + [mode === 'dark' ? palette.salt['90'] : palette.pepper['10']], + invertedCta, + ); case Theme.Cabbage: case Theme.Ketchup: case Theme.Water: return invert([palette[theme][shade]]); case Theme.Pepper: return mode === 'dark' - ? { fills: [palette.pepper['10']], ink: WHITE, cta: WHITE } - : { fills: [palette.salt['90']], ink: PEPPER, cta: PEPPER }; + ? { fills: [palette.pepper['10']], ink: WHITE, cta: themeCta } + : { fills: [palette.salt['90']], ink: PEPPER, cta: themeCta }; case Theme.Background: return mode === 'dark' - ? { fills: [PEPPER], ink: WHITE, cta: WHITE } - : { fills: [WHITE], ink: PEPPER, cta: PEPPER }; + ? { fills: [PEPPER], ink: WHITE, cta: themeCta } + : { fills: [WHITE], ink: PEPPER, cta: themeCta }; case Theme.BlueCheese: return pepper('blueCheese'); case Theme.Avocado: @@ -612,11 +616,12 @@ export const Brief: StoryObj = { fills. It now takes the bar's own text color on every theme.
  • - CTA. It followed the app theme (white button in - dark, dark button in light), which put a white button on the white - bar and on the pale fills. It now follows the bar: pale bar, dark - button; deep bar, white button; white-pepper flips with the theme. - No more brand-colored button on white-pepper. + CTA. White in dark mode on every bar. In light + mode it stays white on the bars whose text is white (the purples, + blue, red, brown) and goes dark on the pale bars whose text is + dark. The two neutral fills that flip with the theme (white-pepper + and salt) invert it so it never matches the bar. No more + brand-colored button on white-pepper.
  • Ink. Bun and bacon move to dark text, matching @@ -686,7 +691,7 @@ export const BeforeAfter: StoryObj = {
    {(mode) => (