/** * PillButton — rounded-full h-[56px] 공용 버튼 (Phase B) * Stitch 샘플의 CTA 버튼 스타일: Primary, Secondary, Outlined 변형 지원 */ import styled from 'styled-components'; import MatIcon from './MatIcon'; export type PillButtonVariant = 'primary' | 'secondary' | 'outlined' | 'text'; export type PillButtonSize = 'default' | 'sm' | 'lg'; interface PillButtonProps extends React.ButtonHTMLAttributes { /** 버튼 변형 */ variant?: PillButtonVariant; /** 크기 (기본: default - h-[56px]) */ size?: PillButtonSize; /** 전체 너비 사용 (기본: true) */ fullWidth?: boolean; /** 왼쪽 아이콘 (MatIcon name) */ leftIcon?: string; /** 오른쪽 아이콘 (MatIcon name) */ rightIcon?: string; /** 로딩 상태 */ loading?: boolean; /** 비활성화 시에도 클릭 이벤트 전달 방지용 (styled-components에서 처리) */ } const VARIANT_STYLES = { primary: { bg: 'var(--md3-primary)', color: 'var(--md3-on-primary)', hoverBg: 'var(--md3-primary-container)', activeBg: 'var(--md3-primary-container)', border: 'none', }, secondary: { bg: 'var(--md3-secondary-container)', color: 'var(--md3-on-secondary-container)', hoverBg: 'var(--md3-secondary)', activeBg: 'var(--md3-secondary)', border: 'none', }, outlined: { bg: 'transparent', color: 'var(--md3-primary)', hoverBg: 'var(--md3-primary-container)', activeBg: 'var(--md3-primary-container)', border: '1px solid var(--md3-outline)', }, text: { bg: 'transparent', color: 'var(--md3-primary)', hoverBg: 'var(--md3-primary-container)', activeBg: 'var(--md3-primary-container)', border: 'none', }, }; const SIZE_STYLES = { default: { height: '56px', px: '24px', fontSize: '14px', fontWeight: 600, gap: '8px' }, sm: { height: '40px', px: '16px', fontSize: '13px', fontWeight: 600, gap: '6px' }, lg: { height: '64px', px: '32px', fontSize: '16px', fontWeight: 600, gap: '10px' }, }; const StyledButton = styled.button<{ $variant: PillButtonVariant; $size: PillButtonSize; $fullWidth: boolean; }>` display: inline-flex; align-items: center; justify-content: center; gap: ${(p) => SIZE_STYLES[p.$size].gap}; height: ${(p) => SIZE_STYLES[p.$size].height}; padding: 0 ${(p) => SIZE_STYLES[p.$size].px}; border-radius: 9999px; border: ${(p) => VARIANT_STYLES[p.$variant].border}; background: ${(p) => VARIANT_STYLES[p.$variant].bg}; color: ${(p) => VARIANT_STYLES[p.$variant].color}; font-family: 'Inter', 'Pretendard', system-ui, sans-serif; font-size: ${(p) => SIZE_STYLES[p.$size].fontSize}; font-weight: ${(p) => SIZE_STYLES[p.$size].fontWeight}; letter-spacing: 0.02em; line-height: 1; cursor: pointer; transition: background-color 0.15s, opacity 0.15s, transform 0.1s; width: ${(p) => (p.$fullWidth ? '100%' : 'auto')}; box-sizing: border-box; white-space: nowrap; box-shadow: ${(p) => p.$variant === 'primary' ? '0 4px 12px rgba(0, 104, 95, 0.25)' : p.$variant === 'secondary' ? '0 2px 8px rgba(74, 99, 96, 0.15)' : 'none'}; &:hover:not(:disabled) { background: ${(p) => VARIANT_STYLES[p.$variant].hoverBg}; } &:active:not(:disabled) { background: ${(p) => VARIANT_STYLES[p.$variant].activeBg}; transform: scale(0.98); } &:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--md3-primary-container); } &:disabled { opacity: 0.45; cursor: not-allowed; background: ${(p) => p.$variant === 'primary' || p.$variant === 'secondary' ? 'var(--md3-outline-variant)' : 'transparent'}; color: var(--md3-on-surface-variant); box-shadow: none; } @media (hover: none) and (pointer: coarse) { &:hover:not(:disabled) { background: ${(p) => VARIANT_STYLES[p.$variant].bg}; } &:active:not(:disabled) { background: ${(p) => VARIANT_STYLES[p.$variant].activeBg}; } } `; const Spinner = styled.span` width: 20px; height: 20px; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: spin 0.6s linear infinite; @keyframes spin { to { transform: rotate(360deg); } } `; export default function PillButton({ children, variant = 'primary', size = 'default', fullWidth = true, leftIcon, rightIcon, loading = false, disabled, className, ...props }: PillButtonProps) { const isDisabled = disabled || loading; return ( {loading ? ( ); }