/** * @title Section Message * @description 화면의 특정 영역에 관련된 중요한 정보나 필요한 조치를 알려주는 메시지예요. */import * as React from 'react';import { cva } from 'class-variance-authority';import { CircleCheck, CircleHelp, Info, OctagonAlert, TriangleAlert, X } from 'lucide-react';import { cn } from '@/lib/utils';import { IconButton } from '@/components/ui/button';// Atlassian @atlaskit/section-message — section-message 스펙// radius.large, padding space.200, 아이콘과 본문 gap space.200, 본문 Stack gap space.100const sectionMessageVariants = cva('rounded-ds-large p-ds-200 break-words', { variants: { appearance: { information: 'bg-ds-background-information', warning: 'bg-ds-background-warning', error: 'bg-ds-background-danger', success: 'bg-ds-background-success', discovery: 'bg-ds-background-discovery', }, }, defaultVariants: { appearance: 'information', },});export type SectionMessageAppearance = 'information' | 'warning' | 'error' | 'success' | 'discovery';const appearanceIcon: Record<SectionMessageAppearance, { Icon: React.ElementType; className: string }> = { information: { Icon: Info, className: 'text-ds-icon-information' }, warning: { Icon: TriangleAlert, className: 'text-ds-icon-warning' }, error: { Icon: OctagonAlert, className: 'text-ds-icon-danger' }, success: { Icon: CircleCheck, className: 'text-ds-icon-success' }, discovery: { Icon: CircleHelp, className: 'text-ds-icon-discovery' },};type HeadingElements = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';export interface SectionMessageProps extends Omit<React.ComponentProps<'section'>, 'title' | 'children'> { /** 메시지 종류. 아이콘과 배경색이 함께 바뀌어요. */ appearance?: SectionMessageAppearance; /** 본문. 한 문단을 권장해요. */ children: React.ReactNode; /** 제목 */ title?: string; /** 제목을 렌더링할 heading 요소 */ headingLevel?: HeadingElements; /** 읽은 뒤 할 수 있는 액션. SectionMessageAction 하나 또는 배열. 여러 개면 가운뎃점으로 구분돼요. */ actions?: React.ReactElement | React.ReactElement<SectionMessageActionProps>[]; /** 기본 아이콘 대신 쓸 아이콘 컴포넌트 */ icon?: React.ElementType; /** 닫기 버튼을 보여줘요. 닫으면 DOM 에서 바로 사라져요. */ isDismissible?: boolean; /** 닫기 버튼을 눌렀을 때 호출 */ onDismiss?: () => void; testId?: string;}function SectionMessage({ appearance = 'information', children, title, headingLevel: HeadingTag = 'h2', actions, icon, isDismissible = false, onDismiss, testId, className, ...props}: SectionMessageProps) { const [dismissed, setDismissed] = React.useState(false); const handleDismiss = React.useCallback(() => { onDismiss?.(); setDismissed(true); }, [onDismiss]); const { Icon: DefaultIcon, className: iconColor } = appearanceIcon[appearance] ?? appearanceIcon.information; const Icon = icon ?? DefaultIcon; const actionElements = actions && !Array.isArray(actions) && actions.type === React.Fragment ? (actions.props as { children?: React.ReactNode }).children : actions; const actionsArray = React.Children.toArray(actionElements); const isList = actionsArray.length > 1; if (isDismissible && dismissed) return null; return ( <section data-slot="section-message" data-appearance={appearance} data-testid={testId} className={cn(sectionMessageVariants({ appearance }), className)} {...props} > <div className="flex items-stretch gap-ds-200"> {/* 아이콘: spacious(24px 영역) + margin-block space.negative.025 */} <div data-slot="section-message-icon" className={cn('-my-ds-025 flex', iconColor)}> <span className="flex size-6 items-center justify-center"> <Icon className="size-4" aria-label={appearance} role="img" /> </span> </div> <div data-slot="section-message-content" data-testid={testId && `${testId}--content`} className="flex min-w-0 grow flex-col gap-ds-100" > {title ? <HeadingTag className="ds-font-heading-small text-ds-text">{title}</HeadingTag> : null} <div className="ds-font-body text-ds-text">{children}</div> {actionsArray.length > 0 && ( <div data-slot="section-message-actions" data-testid={testId && `${testId}--actions`} role={isList ? 'list' : undefined} className="flex flex-wrap items-center gap-x-ds-100 ds-font-body" > {actionsArray.map((action, index) => ( <React.Fragment key={index}> {index > 0 && ( <span aria-hidden className="text-ds-text"> · </span> )} <span role={isList ? 'listitem' : undefined} className="inline-flex"> {action} </span> </React.Fragment> ))} </div> )} </div> {isDismissible && ( <div className="-mt-ds-025"> <IconButton data-testid={testId && `${testId}--dismiss-button`} label="닫기" icon={<X />} appearance="subtle" spacing="compact" onClick={handleDismiss} /> </div> )} </div> </section> );}export interface SectionMessageActionProps { /** 액션 문구 */ children: React.ReactNode; /** 클릭 핸들러. href 가 없으면 버튼으로 렌더링돼요. */ onClick?: (e: React.MouseEvent<HTMLElement>) => void; /** 링크 주소. 있으면 링크로 렌더링돼요. */ href?: string; /** href 가 있을 때 링크의 target */ target?: HTMLAnchorElement['target']; testId?: string; className?: string;}const actionLinkClass = 'rounded-ds-xsmall text-ds-link outline-2 outline-offset-2 outline-transparent focus-visible:outline-ds-border-focused';/** SectionMessage 의 actions 에 넣는 링크·버튼. href 면 링크, onClick 이면 버튼, 둘 다 없으면 텍스트예요. */function SectionMessageAction({ children, onClick, href, target, testId, className }: SectionMessageActionProps) { if (href) { return ( <span data-slot="section-message-action" className="ds-font-body font-ds-regular"> <a data-testid={testId} href={href} target={target} rel={target === '_blank' ? 'noopener noreferrer' : undefined} onClick={onClick} className={cn(actionLinkClass, 'underline hover:no-underline active:text-ds-link-pressed', className)} > {children} </a> </span> ); } if (onClick) { return ( <button type="button" data-slot="section-message-action" data-testid={testId} onClick={onClick} className={cn( actionLinkClass, 'cursor-pointer bg-transparent p-ds-0 ds-font-body font-ds-medium hover:underline active:text-ds-link-pressed', className, )} > {children} </button> ); } return ( <span data-slot="section-message-action" data-testid={testId} className={cn('ds-font-body', className)}> {children} </span> );}export { SectionMessage, SectionMessageAction, sectionMessageVariants };