diff --git a/src/components/CardPrimary/CardPrimary.module.css b/src/components/CardPrimary/CardPrimary.module.css new file mode 100644 index 000000000..f06517044 --- /dev/null +++ b/src/components/CardPrimary/CardPrimary.module.css @@ -0,0 +1,224 @@ +.card-primary { + display: flex; + box-sizing: border-box; + width: 100%; + max-width: 100%; + padding: var(--click-card-primary-space-md-x) var(--click-card-primary-space-md-y); + flex-direction: column; + align-items: center; + gap: var(--click-card-primary-space-md-gap); + border: 1px solid var(--click-card-primary-color-stroke-default); + border-radius: var(--click-card-primary-radii-all); + background-color: var(--click-card-primary-color-background-default); + text-align: center; +} + +.card-primary_size_sm { + padding: var(--click-card-primary-space-sm-x) var(--click-card-primary-space-sm-y); + gap: var(--click-card-primary-space-sm-gap); +} + +.card-primary_size_md { + padding: var(--click-card-primary-space-md-x) var(--click-card-primary-space-md-y); + gap: var(--click-card-primary-space-md-gap); +} + +.card-primary_align_start { + align-items: flex-start; + text-align: left; +} + +.card-primary_align_center { + align-items: center; + text-align: center; +} + +.card-primary_align_end { + align-items: flex-end; + text-align: right; +} + +.card-primary_has-shadow { + box-shadow: var(--click-shadow-1); +} + +.card-primary_is-selected { + border-color: var(--click-button-basic-color-primary-stroke-active); +} + +.card-primary[aria-disabled='true'] { + border-color: var(--click-card-primary-color-stroke-disabled); + background-color: var(--click-card-primary-color-background-disabled); + color: var(--click-card-primary-color-title-disabled); + cursor: not-allowed; + pointer-events: none; +} + +.card-primary__header { + display: flex; + width: 100%; + flex-direction: column; + align-items: center; + gap: var(--click-card-primary-space-md-gap); +} + +.card-primary_size_sm .card-primary__header { + gap: var(--click-card-primary-space-sm-gap); +} + +.card-primary__header_align_start { + align-items: flex-start; +} + +.card-primary__header_align_center { + align-items: center; +} + +.card-primary__header_align_end { + align-items: flex-end; +} + +.card-primary__title { + color: var(--click-global-color-text-default); +} + +.card-primary__title h3 { + color: var(--click-global-color-text-default); +} + +.card-primary__header_disabled .card-primary__title { + color: var(--click-global-color-text-muted); +} + +.card-primary__header_disabled .card-primary__title h3 { + color: var(--click-global-color-text-muted); +} + +.card-primary[aria-disabled='true'] .card-primary__title { + color: var(--click-global-color-text-muted); +} + +.card-primary[aria-disabled='true'] .card-primary__title h3 { + color: var(--click-global-color-text-muted); +} + +.card-primary__icon { + display: flex; + justify-content: center; + align-items: center; +} + +.card-primary .card-primary__icon svg { + width: var(--click-card-primary-size-icon-md-all); + height: var(--click-card-primary-size-icon-md-all); +} + +.card-primary .card-primary__icon img { + width: var(--click-card-primary-size-icon-md-all); + height: var(--click-card-primary-size-icon-md-all); +} + +.card-primary_size_sm .card-primary__icon svg, +.card-primary_size_sm.card-primary .card-primary__icon svg { + width: var(--click-card-primary-size-icon-sm-all); + height: var(--click-card-primary-size-icon-sm-all); +} + +.card-primary_size_sm .card-primary__icon img, +.card-primary_size_sm.card-primary .card-primary__icon img { + width: var(--click-card-primary-size-icon-sm-all); + height: var(--click-card-primary-size-icon-sm-all); +} + +.card-primary__content { + display: flex; + width: 100%; + flex: 1; + flex-direction: column; + align-items: center; + align-self: center; + gap: var(--click-card-primary-space-md-gap); +} + +.card-primary_size_sm .card-primary__content { + gap: var(--click-card-primary-space-sm-gap); +} + +.card-primary__content_align_start { + align-items: flex-start; + align-self: flex-start; +} + +.card-primary__content_align_center { + align-items: center; + align-self: center; +} + +.card-primary__content_align_end { + align-items: flex-end; + align-self: flex-end; +} + +.card-primary__description { + color: var(--click-global-color-text-muted); +} + +.card-primary__button { + width: 100%; + margin-top: auto; +} + +.card-primary__button button { + width: 100%; +} + +.card-primary_align_start .card-primary__button { + align-self: flex-start; +} + +.card-primary_align_center .card-primary__button { + align-self: center; +} + +.card-primary_align_end .card-primary__button { + align-self: flex-end; +} + +.card-primary[aria-disabled='true'] .card-primary__button { + border-color: var(--click-button-basic-color-primary-stroke-disabled); + background-color: var(--click-button-basic-color-primary-background-disabled); +} + +.card-primary:hover:not([aria-disabled='true']) { + background-color: var(--click-card-secondary-color-background-hover); + cursor: pointer; +} + +.card-primary:hover:not([aria-disabled='true']) .card-primary__button button { + border-color: var(--click-button-basic-color-primary-stroke-hover); + background-color: var(--click-button-basic-color-primary-background-hover); +} + +.card-primary:focus-visible:not([aria-disabled='true']) { + background-color: var(--click-card-secondary-color-background-hover); + outline: 2px solid var(--click-button-basic-color-primary-stroke-active); + outline-offset: 2px; +} + +.card-primary:focus-visible:not([aria-disabled='true']) .card-primary__button button { + border-color: var(--click-button-basic-color-primary-stroke-hover); + background-color: var(--click-button-basic-color-primary-background-hover); +} + +.card-primary:focus:not(:focus-visible) { + outline: none; +} + +.card-primary:active:not([aria-disabled='true']) { + border-color: var(--click-button-basic-color-primary-stroke-active); +} + +.card-primary:active:not([aria-disabled='true']) .card-primary__button button { + border-color: var(--click-button-basic-color-primary-stroke-active); + background-color: var(--click-button-basic-color-primary-background-active); +} diff --git a/src/components/CardPrimary/CardPrimary.tsx b/src/components/CardPrimary/CardPrimary.tsx index 7870dc8ab..8bc3df8be 100644 --- a/src/components/CardPrimary/CardPrimary.tsx +++ b/src/components/CardPrimary/CardPrimary.tsx @@ -1,117 +1,42 @@ -import { type MouseEvent } from 'react'; -import { styled } from 'styled-components'; +import { forwardRef } from 'react'; import { Title } from '@/components/Title'; import { Text, type TextAlignment } from '@/components/Text'; import { withTopBadge } from './withTopBadge'; import { Button } from '@/components/Button'; import { Icon } from '@/components/Icon'; import { Spacer } from '@/components/Spacer'; -import { CardPrimaryProps, CardPrimarySize } from './CardPrimary.types'; +import { cn, cva } from '@/lib/cva'; +import styles from './CardPrimary.module.css'; +import { CardPrimaryProps } from './CardPrimary.types'; type ContentAlignment = 'start' | 'center' | 'end'; -const Wrapper = styled.div<{ - $size?: CardPrimarySize; - $hasShadow?: boolean; - $isSelected?: boolean; - $alignContent?: ContentAlignment; -}>` - background-color: ${({ theme }) => theme.click.card.primary.color.background.default}; - border-radius: ${({ theme }) => theme.click.card.primary.radii.all}; - border: ${({ theme }) => `1px solid ${theme.click.card.primary.color.stroke.default}`}; - display: flex; - width: 100%; - max-width: 100%; - text-align: ${({ $alignContent }) => - $alignContent === 'start' ? 'left' : $alignContent === 'end' ? 'right' : 'center'}; - flex-direction: column; - padding: ${({ $size = 'md', theme }) => - `${theme.click.card.primary.space[$size].x} ${theme.click.card.primary.space[$size].y}`}; - gap: ${({ $size = 'md', theme }) => theme.click.card.primary.space[$size].gap}; - box-shadow: ${({ $hasShadow, theme }) => ($hasShadow ? theme.shadow[1] : 'none')}; - - &:hover, - &:focus-visible { - background-color: ${({ theme }) => theme.click.card.secondary.color.background.hover}; - cursor: pointer; - button { - background-color: ${({ theme }) => - theme.click.button.basic.color.primary.background.hover}; - border-color: ${({ theme }) => theme.click.button.basic.color.primary.stroke.hover}; - &:active { - background-color: ${({ theme }) => - theme.click.button.basic.color.primary.background.active}; - border-color: ${({ theme }) => - theme.click.button.basic.color.primary.stroke.active}; - } - } - } - - &:active { - border-color: ${({ theme }) => theme.click.button.basic.color.primary.stroke.active}; - } - - &[aria-disabled='true'], - &[aria-disabled='true']:hover, - &[aria-disabled='true']:focus, - &[aria-disabled='true']:active { - pointer-events: none; - ${({ theme }) => ` - background-color: ${theme.click.card.primary.color.background.disabled}; - color: ${theme.click.card.primary.color.title.disabled}; - border: 1px solid ${theme.click.card.primary.color.stroke.disabled}; - cursor: not-allowed; - - button { - background-color: ${theme.click.button.basic.color.primary.background.disabled}; - border-color: ${theme.click.button.basic.color.primary.stroke.disabled}; - &:active { - background-color: ${theme.click.button.basic.color.primary.background.disabled}; - border-color: ${theme.click.button.basic.color.primary.stroke.disabled}; - } - }`} - } - - ${({ $isSelected, theme }) => - $isSelected - ? `border-color: ${theme.click.button.basic.color.primary.stroke.active};` - : ''} -`; - -const Header = styled.div<{ - $size?: 'sm' | 'md'; - $disabled?: boolean; - $alignContent?: ContentAlignment; -}>` - display: flex; - flex-direction: column; - align-items: ${({ $alignContent = 'center' }) => - ['start', 'end'].includes($alignContent) ? `flex-${$alignContent}` : $alignContent}; - gap: ${({ $size = 'md', theme }) => theme.click.card.primary.space[$size].gap}; - - h3 { - color: ${({ $disabled, theme }) => - $disabled == true - ? theme.click.global.color.text.muted - : theme.click.global.color.text.default}; - } - - svg, - img { - height: ${({ $size = 'md', theme }) => theme.click.card.primary.size.icon[$size].all}; - width: ${({ $size = 'md', theme }) => theme.click.card.primary.size.icon[$size].all}; - } -`; - -const Content = styled.div<{ $size?: 'sm' | 'md'; $alignContent?: ContentAlignment }>` - width: 100%; - display: flex; - flex-direction: column; - align-self: ${({ $alignContent = 'center' }) => - ['start', 'end'].includes($alignContent) ? `flex-${$alignContent}` : $alignContent}; - gap: ${({ $size = 'md', theme }) => theme.click.card.primary.space[$size].gap}; - flex: 1; -`; +const cardVariants = cva(styles['card-primary'], { + variants: { + size: { + sm: styles['card-primary_size_sm'], + md: styles['card-primary_size_md'], + }, + align: { + start: styles['card-primary_align_start'], + center: styles['card-primary_align_center'], + end: styles['card-primary_align_end'], + }, + hasShadow: { + true: styles['card-primary_has-shadow'], + }, + isSelected: { + true: styles['card-primary_is-selected'], + }, + disabled: { + true: undefined, + }, + }, + defaultVariants: { + size: 'md', + align: 'center', + }, +}); const convertCardAlignToTextAlign = (align: ContentAlignment): TextAlignment => { if (align === 'center') { @@ -120,99 +45,134 @@ const convertCardAlignToTextAlign = (align: ContentAlignment): TextAlignment => return align === 'start' ? 'left' : 'right'; }; -const Card = ({ - alignContent, - title, - icon, - iconUrl, - hasShadow = false, - description, - infoUrl, - infoText, - size, - disabled = false, - onButtonClick, - isSelected, - children, - ...props -}: CardPrimaryProps) => { - const handleClick = (e: MouseEvent) => { - if (typeof onButtonClick === 'function') { - onButtonClick(e); - } - if (infoUrl && infoUrl.length > 0) { - window.open(infoUrl, '_blank'); - } - }; - - const hasAction = !!infoUrl || typeof onButtonClick === 'function'; - const Component = hasAction ? Button : 'div'; - const hasConsumerClick = typeof props.onClick === 'function'; - - return ( - - {(icon || title) && ( -
- {iconUrl ? ( - card icon - ) : ( - icon && ( - - ) - )} - {title && {title}} -
- )} - - {(description || children) && ( - - {description && ( - ( + ( + { + alignContent, + title, + icon, + iconUrl, + hasShadow = false, + description, + infoUrl, + infoText, + size = 'md', + disabled = false, + onButtonClick, + isSelected, + children, + className, + ...props + }, + ref + ) => { + const handleClick = (e: React.MouseEvent) => { + if (disabled) { + return; + } + if (typeof onButtonClick === 'function') { + onButtonClick(e); + } + if (infoUrl && infoUrl.length > 0) { + window.open(infoUrl, '_blank'); + } + }; + + const hasAction = !!infoUrl || typeof onButtonClick === 'function'; + const Component = hasAction ? Button : 'div'; + const contentAlign = alignContent ?? 'center'; + const hasConsumerClick = typeof props.onClick === 'function'; + + return ( +
+ {(icon || title) && ( +
+ {(icon || iconUrl) && ( +
+ {iconUrl ? ( + card icon + ) : ( + icon && ( + + ) + )} +
+ )} + {title && ( +
+ {title} +
+ )} +
+ )} + + {(description || children) && ( +
+ {description && ( +
+ + {description} + +
+ )} + {children} +
+ )} + + {size === 'sm' && } + + {infoText && ( +
+ - {description} - - )} - {children} - - )} - - {size == 'sm' && } + {infoText} + +
+ )} +
+ ); + } +); - {infoText && ( - - {infoText} - - )} -
- ); -}; +Card.displayName = 'CardPrimary'; export const CardPrimary = withTopBadge(Card); diff --git a/src/components/CardPrimary/CardPrimaryTopBadge.module.css b/src/components/CardPrimary/CardPrimaryTopBadge.module.css new file mode 100644 index 000000000..6489d6e33 --- /dev/null +++ b/src/components/CardPrimary/CardPrimaryTopBadge.module.css @@ -0,0 +1,25 @@ +.top-badge-wrapper { + display: flex; + position: relative; + width: 100%; + flex-direction: column; + align-items: stretch; +} + +.card-primary-top-badge { + position: absolute; + top: 0; + left: 50%; + border: 1px solid transparent; + transform: translate(-50%, -50%); +} + +.card-primary-top-badge_is-selected { + border-color: var(--click-button-basic-color-primary-stroke-active) !important; +} + +/* When the card is active, also highlight the badge */ +.card-primary-top-badge:active, +.card-primary:active ~ .card-primary-top-badge { + border-color: var(--click-button-basic-color-primary-stroke-active) !important; +} diff --git a/src/components/CardPrimary/CardPrimaryTopBadge.tsx b/src/components/CardPrimary/CardPrimaryTopBadge.tsx index 30c035c80..2ea24244d 100644 --- a/src/components/CardPrimary/CardPrimaryTopBadge.tsx +++ b/src/components/CardPrimary/CardPrimaryTopBadge.tsx @@ -1,21 +1,26 @@ import { Badge } from '@/components/Badge'; -import { Container } from '@/components/Container'; -import { styled } from 'styled-components'; +import { cn } from '@/lib/cva'; +import styles from './CardPrimaryTopBadge.module.css'; -export const TopBadgeWrapper = styled(Container)` - position: relative; -`; +interface CardPrimaryTopBadgeProps { + text: string; + isSelected?: boolean; + 'data-testid'?: string; +} -export const CardPrimaryTopBadge = styled(Badge)<{ $isSelected?: boolean }>` - position: absolute; - top: 0; - left: 50%; - transform: translate(-50%, -50%); - ${({ $isSelected, theme }) => - $isSelected - ? `border-color: ${theme.click.button.basic.color.primary.stroke.active};` - : ''} - div:active + & { - border-color: ${({ theme }) => theme.click.button.basic.color.primary.stroke.active}; - } -`; +export const CardPrimaryTopBadge = ({ + text, + isSelected, + 'data-testid': dataTestId, +}: CardPrimaryTopBadgeProps) => { + return ( + + ); +}; diff --git a/src/components/CardPrimary/withTopBadge.tsx b/src/components/CardPrimary/withTopBadge.tsx index 061fa413c..34964b263 100644 --- a/src/components/CardPrimary/withTopBadge.tsx +++ b/src/components/CardPrimary/withTopBadge.tsx @@ -1,5 +1,6 @@ -import { CardPrimaryTopBadge, TopBadgeWrapper } from './CardPrimaryTopBadge'; +import { CardPrimaryTopBadge } from './CardPrimaryTopBadge'; import { ComponentType, FC } from 'react'; +import styles from './CardPrimaryTopBadge.module.css'; export interface WithTopBadgeProps { topBadgeText?: string; @@ -10,18 +11,15 @@ export const withTopBadge =

(Component: ComponentType

): FC

=> ({ topBadgeText, ...props }: P & WithTopBadgeProps) => { return ( - +

{topBadgeText && ( )} - +
); }; diff --git a/tests/cards/cardprimary.spec.ts-snapshots/card-primary-focus-light-chromium-linux.png b/tests/cards/cardprimary.spec.ts-snapshots/card-primary-focus-light-chromium-linux.png index 5d16656c3..f32877d69 100644 Binary files a/tests/cards/cardprimary.spec.ts-snapshots/card-primary-focus-light-chromium-linux.png and b/tests/cards/cardprimary.spec.ts-snapshots/card-primary-focus-light-chromium-linux.png differ