@patreon/studio
Version:
Patreon Studio Design System
241 lines (226 loc) • 8.9 kB
JSX
'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