/** * @title Input Button * @description 입력 필드 형태의 버튼으로, 선택창이나 피커를 열고 선택된 값을 라벨에 보여줘요. */'use client';import * as React from 'react';import { cva } from 'class-variance-authority';import { cn } from '@/lib/utils';// SEED Rootage spec: input-button.yamltype InputButtonSize = 'large' | 'medium' | 'responsive';const inputButtonVariants = cva( [ 'group/input-button relative isolate box-border flex w-full items-center antialiased', // value / placeholder / affix 는 pointer-events 를 꺼서 아래 button 이 눌리게 해요. '[&>[data-part]]:pointer-events-none', // scaleScope: content '[&>[data-part]]:transition-[scale] [&>[data-part]]:duration-(--rb-duration-pressed-scale) [&>[data-part]]:ease-pressed-scale', 'has-[[data-slot=input-button-button]:enabled:not([aria-readonly]):active]:[&>[data-part]]:scale-[0.97]', ], { variants: { size: { large: 'h-x13 gap-x2_5 px-x4 text-t5', medium: 'h-x10 gap-x2 px-x3_5 text-t4', responsive: 'h-x13 gap-x2_5 px-x4 text-t5 lg:h-x10 lg:gap-x2 lg:px-x3_5 lg:text-t4', }, }, defaultVariants: { size: 'large' }, },);const inputButtonButtonVariants = cva( [ 'absolute inset-0 -z-10 m-0 cursor-pointer border-none bg-bg-transparent p-0', 'shadow-[inset_0_0_0_1px_var(--rb-color-stroke-neutral-weak)]', 'transition-[background-color,outline-color] duration-(--rb-duration-color-transition) ease-easing', 'outline-2 outline-offset-2 outline-transparent focus-visible:outline-stroke-focus-ring', // invalid stroke "after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:border-2 after:border-transparent after:content-['']", 'after:transition-[border-color] after:duration-100 after:ease-easing', 'aria-invalid:after:border-stroke-critical-solid', // pressed 'not-aria-readonly:enabled:active:bg-bg-transparent-pressed', // readonly / disabled 'aria-readonly:cursor-default aria-readonly:bg-bg-disabled', 'disabled:cursor-not-allowed disabled:bg-bg-disabled', ], { variants: { size: { large: 'rounded-r3', medium: 'rounded-r2', responsive: 'rounded-r3 lg:rounded-r2', }, }, defaultVariants: { size: 'large' }, },);const iconSize = { large: 'size-x5', medium: 'size-x4', responsive: 'size-x5 lg:size-x4' } as const;const clearButtonSize = { large: 'size-[22px]', medium: 'size-x4_5', responsive: 'size-[22px] lg:size-x4_5',} as const;export interface InputButtonProps extends Omit<React.ComponentProps<'button'>, 'prefix' | 'value'> { /** * `responsive` 는 `lg` 미만에서 large, 이상에서 medium 으로 적용돼요. * @default 'large' */ size?: InputButtonSize; /** 선택된 값이에요. 비어 있으면 placeholder 를 보여줘요. */ value?: React.ReactNode; placeholder?: React.ReactNode; prefixIcon?: React.ReactNode; /** 값 앞에 붙는 텍스트예요. */ prefix?: React.ReactNode; /** 값 뒤에 붙는 텍스트예요. */ suffix?: React.ReactNode; suffixIcon?: React.ReactNode; /** Form 검증 실패 상태예요. */ invalid?: boolean; /** 값을 보여주지만 바꿀 수 없어요. */ readOnly?: boolean; /** 값이 있을 때 지우기 버튼을 보여줘요. */ showClearButton?: boolean; onClear?: () => void; /** @default '지우기' */ clearButtonLabel?: string; rootClassName?: string; rootRef?: React.Ref<HTMLDivElement>;}function hasValue(value: React.ReactNode) { if (value === null || value === undefined || value === false || value === '') return false; if (Array.isArray(value)) return value.length > 0; return true;}function InputButton({ size = 'large', value, placeholder, prefixIcon, prefix, suffix, suffixIcon, invalid = false, readOnly = false, disabled = false, showClearButton = false, onClear, clearButtonLabel = '지우기', className, rootClassName, rootRef, onClick, children, ...props}: InputButtonProps) { const filled = hasValue(value); const renderClear = showClearButton && filled && !disabled && !readOnly; return ( <div ref={rootRef} data-slot="input-button" data-size={size} data-disabled={disabled || undefined} data-readonly={readOnly || undefined} data-invalid={invalid || undefined} className={cn(inputButtonVariants({ size }), rootClassName)} > <button type="button" data-slot="input-button-button" disabled={disabled} aria-invalid={invalid || undefined} aria-readonly={readOnly || undefined} onClick={(event) => { if (readOnly) { event.preventDefault(); return; } onClick?.(event); }} className={cn(inputButtonButtonVariants({ size }), className)} {...props} > <span className="sr-only">{filled ? value : placeholder}</span> </button> {prefixIcon && ( <span data-part data-slot="input-button-prefix-icon" aria-hidden className={cn( 'inline-flex shrink-0 text-fg-neutral-muted group-data-[disabled]/input-button:text-fg-disabled [&_svg]:size-full', iconSize[size], )} > {prefixIcon} </span> )} {prefix && ( <span data-part data-slot="input-button-prefix-text" aria-hidden className="shrink-0 font-regular text-fg-neutral-subtle group-data-[disabled]/input-button:text-fg-disabled" > {prefix} </span> )} {filled ? ( <span data-part data-slot="input-button-value" aria-hidden className="min-w-0 grow truncate font-regular text-fg-neutral group-data-[disabled]/input-button:text-fg-disabled" > {value} </span> ) : ( <span data-part data-slot="input-button-placeholder" aria-hidden className="min-w-0 grow truncate font-regular text-fg-placeholder group-data-[disabled]/input-button:text-fg-disabled" > {placeholder} </span> )} {children} {renderClear && ( <button type="button" data-slot="input-button-clear-button" aria-label={clearButtonLabel} onClick={onClear} className={cn( 'inline-flex shrink-0 cursor-pointer items-center justify-center rounded-full border-none bg-transparent p-0 text-fg-neutral-subtle', 'transition-[scale,outline-color] duration-(--rb-duration-pressed-scale) ease-pressed-scale active:scale-[0.95]', 'outline-2 outline-offset-2 outline-transparent focus-visible:outline-stroke-focus-ring', clearButtonSize[size], )} > <svg viewBox="0 0 24 24" className="size-full" aria-hidden> <circle cx="12" cy="12" r="11" fill="currentColor" /> <path d="m8.5 8.5 7 7m0-7-7 7" stroke="var(--rb-color-bg-layer-default)" strokeWidth="2" strokeLinecap="round" /> </svg> </button> )} {suffix && ( <span data-part data-slot="input-button-suffix-text" aria-hidden className="shrink-0 font-regular text-fg-neutral-subtle group-data-[disabled]/input-button:text-fg-disabled" > {suffix} </span> )} {suffixIcon && ( <span data-part data-slot="input-button-suffix-icon" aria-hidden className={cn( 'inline-flex shrink-0 text-fg-neutral-muted group-data-[disabled]/input-button:text-fg-disabled [&_svg]:size-full', iconSize[size], )} > {suffixIcon} </span> )} </div> );}export { InputButton, inputButtonVariants };