UNPKG

@patreon/studio

Version:

Patreon Studio Design System

241 lines (226 loc) 8.9 kB
'use client'; import React from 'react'; import styled, { css } from 'styled-components'; import { useSequentialId } from '../../hooks/useSequentialId'; import { tokens } from '../../tokens'; import { mediaForBreakpoint } from '../../utilities/breakpoints'; import { report } from '../../utilities/deprecation'; import { cssForBoldBodyText } from '../../utilities/type-bundles'; import { BodyText } from '../BodyText'; import { Button } from '../Button'; import { HeadingText } from '../HeadingText'; import { IconClose } from '../Icon'; import { LoadingSpinner } from '../LoadingSpinner'; import { TextLink } from '../TextLink'; export function Banner({ action, secondaryAction, alignAction = 'right', children, header, inlineLink, contentDirection = 'column', 'data-tag': dataTag, id, inline, placement, onClose, variant = 'info', icon, loading = false, }) { const contentId = useSequentialId('BannerContent'); const Icon = icon; const hasIcon = !!Icon || loading; const iconSize = placement === 'inline-small' || loading ? 20 : 40; const hasOnClose = !!onClose; const hasAction = !!action || !!secondaryAction; const isSingleLine = !header && !inlineLink; const hasFloatingCloseButton = !isSingleLine && onClose && !action; const hasInlineCloseButton = isSingleLine && onClose && !action; if (inline) { report('`inline` property is deprecated, use the `placement` prop instead'); } const role = variant === 'critical' || variant === 'warning' ? 'alert' : 'status'; const computedIcon = (Icon || loading) && (<AccessoryWrapper> {Icon && !loading && (<IconWrapper variant={variant} placement={placement}> <Icon size={{ xs: '20px', md: placement === 'inline-large' ? '24px' : '20px', }} color="inherit"/> </IconWrapper>)} {loading && <LoadingSpinner size="xs"/>} </AccessoryWrapper>); const closeButton = hasInlineCloseButton && (<CloseButtonWrapper> <Button onClick={onClose} size="sm" aria-label="close" icon={IconClose} variant="tertiary" unfilled corners="pill"/> </CloseButtonWrapper>); return (<Container aria-describedby={contentId} aria-live="polite" data-tag={dataTag} id={id} onMouseDown={(e) => e.preventDefault()} role={role} placement={placement} tabIndex={0} variant={variant}> {hasFloatingCloseButton && (<FloatingCloseButton onClick={onClose} size="sm" aria-label="close" icon={IconClose} variant="tertiary" unfilled corners="pill"/>)} <ContentWrapper placement={placement} isSingleLine={isSingleLine} hasOnClose={hasOnClose} hasAction={hasAction} alignAction={alignAction}> {computedIcon} <Content alignAction={alignAction}> <TextWrapper variant={variant} contentDirection={contentDirection}> {header && (<HeadingText as="h3" size="md" color={getBannerTextColor({ variant })}> {header} </HeadingText>)} <BodyText as="div" size="md" color={getBannerTextColor({ variant })}> {children} </BodyText> {inlineLink && (<TextLink size="md" {...inlineLink}> {inlineLink.label} </TextLink>)} </TextWrapper> {(secondaryAction || action) && (<ActionWrapper hasIcon={hasIcon} iconSize={iconSize} alignAction={alignAction}> {secondaryAction && (<Button data-tag="secondary-action" variant={variant === 'inverted' ? 'insetWhite' : 'tertiary'} size="md" {...secondaryAction}> {secondaryAction.label} </Button>)} {action && (<Button data-tag="action" variant="insetWhite" size="md" {...action}> {action.label} </Button>)} </ActionWrapper>)} </Content> {closeButton} </ContentWrapper> </Container>); } const getBannerColor = ({ variant, background }) => { if (background) { if (variant === 'info') { return tokens.global.primary.subtle; } if (variant === 'inverted') { return tokens.global.content.regular; } return tokens.global[variant].muted; } if (variant === 'info') { return tokens.global.primary.action; } if (variant === 'inverted') { return tokens.global.content.inverted; } return tokens.global[variant].action; }; const getBannerTextColor = ({ variant }) => { if (variant === 'inverted') { return tokens.global.content.inverted.default; } return tokens.global.content.regular.default; }; const getBannerIconColors = ({ variant, placement }) => { if (placement === 'inline-large') { if (variant === 'info') { return { backgroundColor: tokens.global.content.invertedMuted.default, iconColor: tokens.global.content.regular.default, }; } return { backgroundColor: tokens.global.content.invertedMuted.default, iconColor: getBannerColor({ variant }).default, }; } return { backgroundColor: 'transparent', iconColor: tokens.global.content.regular.default }; }; const paddingMap = { mobile: { 'inline-large': tokens.global.space.x16, 'inline-small': tokens.global.space.x12, global: tokens.global.space.x16, }, desktop: { 'inline-large': tokens.global.space.x24, 'inline-small': tokens.global.space.x16, global: tokens.global.space.x16, }, }; const Container = styled.div ` background-color: ${({ variant }) => getBannerColor({ variant, background: true }).default}; border-radius: ${({ placement }) => (placement === 'global' ? 0 : tokens.global.radius.md)}; position: relative; overflow: hidden; padding: ${({ placement }) => paddingMap.mobile[placement]}; @media ${mediaForBreakpoint('md')} { padding: ${({ placement }) => paddingMap.desktop[placement]}; } `; const ContentWrapper = styled.div ` display: flex; justify-content: space-between; align-items: flex-start; flex-wrap: no-wrap; width: 100%; gap: ${tokens.global.space.x16}; @media ${mediaForBreakpoint('md')} { align-items: ${({ placement, alignAction, isSingleLine }) => placement === 'inline-large' || (alignAction !== 'bottom' && isSingleLine) ? 'center' : 'flex-start'}; } `; const Content = styled.div ` display: flex; flex-direction: column; gap: ${tokens.global.space.x16}; width: 100%; @media ${mediaForBreakpoint('md')} { flex-direction: ${({ alignAction }) => (alignAction === 'bottom' ? 'column' : 'row')}; align-items: ${({ alignAction }) => (alignAction !== 'bottom' ? 'center' : 'flex-start')}; } `; const TextWrapper = styled.div ` display: flex; flex-direction: column; align-items: space-between; word-wrap: break-word; gap: ${tokens.global.space.x4}; width: 100%; // TODO: This is to fix a rendering issue in Studio docs that would ideally // be fixed in the docs themselves. We should remove this once the docs handle core // element styles properly. p { margin: 0; } a { ${cssForBoldBodyText()} --TextLink-color-default: ${({ variant }) => getBannerColor({ variant }).default}; --TextLink-color-hover: ${({ variant }) => getBannerColor({ variant }).hover}; --TextLink-color-pressed: ${({ variant }) => getBannerColor({ variant }).pressed}; } @media ${mediaForBreakpoint('md')} { flex-direction: ${({ contentDirection }) => contentDirection}; align-items: ${({ contentDirection }) => contentDirection === 'row' && 'center'}; } `; const ActionWrapper = styled.div ` display: flex; gap: ${tokens.global.space.x8}; flex-wrap: wrap; @media ${mediaForBreakpoint('md')} { flex-wrap: nowrap; } `; const AccessoryWrapper = styled.div ` display: flex; `; const IconWrapper = styled.div ` display: flex; align-items: center; justify-content: center; ${({ placement }) => placement === 'inline-large' && css ` width: 28px; height: 28px; border-radius: ${tokens.global.radius.circle}; @media ${mediaForBreakpoint('md')} { width: 40px; height: 40px; } `} ${({ placement }) => placement === 'inline-small' && css ` padding-top: 1px; padding-left: 1px; `} ${({ placement, variant }) => { const { backgroundColor, iconColor } = getBannerIconColors({ variant, placement }); return css ` background-color: ${backgroundColor}; color: ${iconColor}; `; }} `; const FloatingCloseButton = styled(Button) ` position: absolute; top: ${tokens.global.space.x8}; right: ${tokens.global.space.x8}; `; const CloseButtonWrapper = styled.div ` display: flex; flex-direction: column; align-items: flex-start; margin-top: -6px; margin-bottom: -6px; `; //# sourceMappingURL=index.jsx.map