/** * @title Action Button * @description 명확한 액션을 쉽게 수행할 수 있도록 돕는 기본 인터랙션 컴포넌트예요. */import * as React from 'react';import { Slot } from 'radix-ui';import { cva, type VariantProps } from 'class-variance-authority';import { cn } from '@/lib/utils';// SEED Rootage spec: action-button.yamlconst actionButtonVariants = cva( [ 'relative inline-flex shrink-0 cursor-pointer select-none items-center justify-center whitespace-nowrap align-middle font-bold antialiased', 'transition-[background-color,scale,outline-color] duration-(--rb-duration-color-transition) ease-easing', 'outline-2 outline-offset-2 outline-transparent focus-visible:outline-stroke-focus-ring', 'enabled:active:scale-[0.97] data-[loading]:active:scale-100', 'disabled:cursor-not-allowed disabled:bg-bg-disabled disabled:text-fg-disabled', '[&_svg]:pointer-events-none [&_svg]:shrink-0', ], { variants: { variant: { brandSolid: 'bg-bg-brand-solid text-fg-on-brand-solid enabled:active:bg-bg-brand-solid-pressed data-[loading]:bg-bg-brand-solid-pressed', neutralSolid: 'bg-bg-neutral-solid text-fg-on-neutral-solid enabled:active:bg-bg-neutral-solid-pressed data-[loading]:bg-bg-neutral-solid-pressed', neutralWeak: 'bg-bg-neutral-weak text-fg-neutral enabled:active:bg-bg-neutral-weak-pressed data-[loading]:bg-bg-neutral-weak-pressed', criticalSolid: 'bg-bg-critical-solid text-fg-on-critical-solid enabled:active:bg-bg-critical-solid-pressed data-[loading]:bg-bg-critical-solid-pressed', neutralOutline: 'border border-stroke-neutral-muted bg-bg-transparent text-fg-neutral enabled:active:bg-bg-transparent-pressed disabled:bg-bg-transparent', brandOutline: 'border border-stroke-neutral-muted bg-bg-transparent text-fg-brand enabled:active:bg-bg-transparent-pressed disabled:bg-bg-transparent', ghost: 'bg-transparent text-fg-neutral enabled:active:bg-bg-transparent-pressed disabled:bg-transparent', }, size: { xsmall: 'min-h-x8 rounded-full', small: 'min-h-x9 rounded-r2', medium: 'min-h-x10 rounded-r2', large: 'min-h-x13 rounded-r3', }, layout: { withText: '', iconOnly: '', }, }, compoundVariants: [ { size: 'xsmall', layout: 'withText', className: 'gap-x1 px-x3_5 py-x1_5 text-t3 [&_svg]:size-x3_5' }, { size: 'xsmall', layout: 'iconOnly', className: 'min-w-x8 p-x1_5 [&_svg]:size-x3_5' }, { size: 'small', layout: 'withText', className: 'gap-x1 px-x3_5 py-x2 text-t4 [&_svg]:size-x3_5' }, { size: 'small', layout: 'iconOnly', className: 'min-w-x9 p-x2 [&_svg]:size-x4' }, { size: 'medium', layout: 'withText', className: 'gap-x1_5 px-x4 py-x2_5 text-t4 [&_svg]:size-x4' }, { size: 'medium', layout: 'iconOnly', className: 'min-w-x10 p-x2_5 [&_svg]:size-[18px]' }, { size: 'large', layout: 'withText', className: 'gap-x2 px-x5 py-x3_5 text-t6 [&_svg]:size-[22px]' }, { size: 'large', layout: 'iconOnly', className: 'min-w-x13 p-x3_5 [&_svg]:size-[22px]' }, ], defaultVariants: { variant: 'brandSolid', size: 'medium', layout: 'withText', }, },);const spinnerSize = { xsmall: 14, small: 14, medium: 16, large: 18 } as const;function Spinner({ size }: { size: number }) { return ( <svg className="animate-spin" width={size} height={size} viewBox="0 0 16 16" fill="none" aria-hidden style={{ width: size, height: size }} > <circle cx="8" cy="8" r="7" stroke="currentColor" strokeOpacity="0.3" strokeWidth="2" /> <path d="M8 1a7 7 0 0 1 7 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" /> </svg> );}export interface ActionButtonProps extends React.ComponentProps<'button'>, VariantProps<typeof actionButtonVariants> { asChild?: boolean; /** 로딩 중에는 라벨을 숨기고 스피너를 보여주며 클릭을 막아요. */ loading?: boolean;}function ActionButton({ className, variant, size, layout, asChild = false, loading = false, disabled, children, ...props}: ActionButtonProps) { const Comp = asChild ? Slot.Root : 'button'; return ( <Comp data-slot="action-button" data-loading={loading || undefined} aria-busy={loading || undefined} disabled={disabled} className={cn(actionButtonVariants({ variant, size, layout }), loading && 'pointer-events-none', className)} {...props} > {asChild ? ( children ) : ( <> <span className={cn('inline-flex items-center gap-[inherit]', loading && 'invisible')}>{children}</span> {loading && ( <span className="absolute inset-0 flex items-center justify-center"> <Spinner size={spinnerSize[size ?? 'medium']} /> </span> )} </> )} </Comp> );}export { ActionButton, actionButtonVariants };