'use client';/** * @title Progress Board * @description 항목별·플랫폼별 진행 상태를 표 형태로 한눈에 보여줘요. */import * as React from 'react';import { Tooltip as TooltipPrimitive } from 'radix-ui';import { cva, type VariantProps } from 'class-variance-authority';import { InfoIcon } from 'lucide-react';import { cn } from '@/lib/utils';// SEED docs: progress-board-table.tsx + table.tsx (SEED 테이블 디자인) + badge.yaml (size=large, variant=weak)export interface ProgressBoardProps extends React.ComponentProps<'table'> { /** 가로 스크롤 시 첫 번째 열(항목 이름)을 고정해요. */ stickyFirstColumn?: boolean; /** 바깥 스크롤 래퍼에 붙일 클래스예요. */ wrapperClassName?: string;}function ProgressBoard({ className, wrapperClassName, stickyFirstColumn = false, ...props }: ProgressBoardProps) { return ( <div data-slot="progress-board-wrapper" className={cn('relative w-full overflow-x-auto', wrapperClassName)}> <table data-slot="progress-board" data-sticky-first-column={stickyFirstColumn || undefined} className={cn( 'w-full border-collapse text-t4 text-fg-neutral', stickyFirstColumn && '[&_td:first-child]:sticky [&_td:first-child]:left-0 [&_td:first-child]:z-1 [&_td:first-child]:bg-bg-layer-default [&_th:first-child]:sticky [&_th:first-child]:left-0 [&_th:first-child]:z-1 [&_th:first-child]:bg-bg-layer-default', className, )} {...props} /> </div> );}function ProgressBoardHeader({ className, ...props }: React.ComponentProps<'thead'>) { return <thead data-slot="progress-board-header" className={cn(className)} {...props} />;}function ProgressBoardBody({ className, ...props }: React.ComponentProps<'tbody'>) { return ( <tbody data-slot="progress-board-body" className={cn( '[&_tr]:transition-colors [&_tr]:duration-(--rb-duration-color-transition) [&_tr]:ease-easing [&_tr:hover]:bg-bg-neutral-weak [&_tr:hover_td:first-child]:bg-bg-neutral-weak', className, )} {...props} /> );}function ProgressBoardRow({ className, ...props }: React.ComponentProps<'tr'>) { return ( <tr data-slot="progress-board-row" className={cn('group border-b border-stroke-neutral-muted', className)} {...props} /> );}const progressBoardCellVariants = cva('px-x4 align-middle', { variants: { align: { start: 'text-left', center: 'text-center', end: 'text-right', }, }, defaultVariants: { align: 'start' },});export interface ProgressBoardHeadProps extends Omit<React.ComponentProps<'th'>, 'align'>, VariantProps<typeof progressBoardCellVariants> {}function ProgressBoardHead({ className, align, ...props }: ProgressBoardHeadProps) { return ( <th data-slot="progress-board-head" scope="col" className={cn( progressBoardCellVariants({ align }), 'whitespace-nowrap py-x2_5 font-medium text-fg-neutral', className, )} {...props} /> );}export interface ProgressBoardCellProps extends Omit<React.ComponentProps<'td'>, 'align'>, VariantProps<typeof progressBoardCellVariants> {}function ProgressBoardCell({ className, align, ...props }: ProgressBoardCellProps) { return ( <td data-slot="progress-board-cell" className={cn(progressBoardCellVariants({ align }), 'py-x3', className)} {...props} /> );}// 상태 배지: badge size=large, variant=weak 토큰 값을 그대로 사용const progressBoardStatusVariants = cva( 'inline-flex min-h-x6 shrink-0 items-center justify-center gap-x0_5 whitespace-nowrap rounded-r1_5 px-x2 py-x1 text-t2 font-medium', { variants: { status: { ready: 'bg-bg-positive-weak text-fg-positive-contrast', inProgress: 'bg-bg-warning-weak text-fg-warning-contrast', notReady: 'bg-bg-neutral-weak text-fg-neutral-muted', deprecated: 'bg-bg-neutral-weak text-fg-neutral-muted line-through', notPlanned: 'bg-bg-neutral-weak text-fg-neutral-subtle', }, }, defaultVariants: { status: 'notReady' }, },);export type ProgressBoardStatusValue = NonNullable<VariantProps<typeof progressBoardStatusVariants>['status']>;const progressBoardStatusLabels: Record<ProgressBoardStatusValue, string> = { ready: 'Done', inProgress: 'In Progress', notReady: 'Not Ready', deprecated: 'Deprecated', notPlanned: 'Not Planned',};export interface ProgressBoardStatusProps extends Omit<React.ComponentProps<'span'>, 'children'>, VariantProps<typeof progressBoardStatusVariants> { /** 기본 라벨(Done, In Progress …) 대신 보여줄 라벨이에요. */ label?: React.ReactNode; /** 상태에 대한 비고예요. 지정하면 옆에 정보 아이콘이 생기고 툴팁으로 보여줘요. */ note?: React.ReactNode;}function ProgressBoardStatus({ className, status, label, note, ...props }: ProgressBoardStatusProps) { const value = status ?? 'notReady'; const badge = ( <span data-slot="progress-board-status" data-status={value} className={cn(progressBoardStatusVariants({ status: value }), className)} {...props} > {label ?? progressBoardStatusLabels[value]} </span> ); if (!note) return badge; return ( <span data-slot="progress-board-status-group" className="inline-flex items-center gap-x1_5"> {badge} <TooltipPrimitive.Provider delayDuration={100}> <TooltipPrimitive.Root> <TooltipPrimitive.Trigger asChild> <button type="button" data-slot="progress-board-status-note" aria-label="비고 보기" className="inline-flex cursor-pointer items-center rounded-full text-fg-neutral-subtle outline-2 outline-offset-2 outline-transparent transition-colors duration-(--rb-duration-color-transition) ease-easing hover:text-fg-neutral focus-visible:outline-stroke-focus-ring" > <InfoIcon className="size-x4" aria-hidden /> </button> </TooltipPrimitive.Trigger> <TooltipPrimitive.Portal> <TooltipPrimitive.Content side="top" sideOffset={6} className="z-50 max-w-60 rounded-r2 bg-bg-neutral-inverted px-x3 py-x2 text-t3 font-medium text-fg-neutral-inverted shadow-s2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0" > {note} <TooltipPrimitive.Arrow className="fill-bg-neutral-inverted" /> </TooltipPrimitive.Content> </TooltipPrimitive.Portal> </TooltipPrimitive.Root> </TooltipPrimitive.Provider> </span> );}export { ProgressBoard, ProgressBoardHeader, ProgressBoardBody, ProgressBoardRow, ProgressBoardHead, ProgressBoardCell, ProgressBoardStatus, progressBoardStatusVariants, progressBoardStatusLabels,};