"use client"; import type { ReactNode, Ref } from "react"; import { cx } from "../../utils"; import styles from "./fox-button.module.scss"; export type FoxButtonType = "primary" | "secondary" | "default" | "error"; export type FoxButtonSize = "lg" | "md" | "sm" | "xsm"; export interface FoxButtonProps { /** 시각 계열. 디자인은 Figma 시안(node 390-439) 기준으로 추후 적용한다. */ type?: FoxButtonType; size: FoxButtonSize; /** 네이티브 `type` 속성. prop 이름의 `type`이 시각 계열을 쓰므로 분리했다. */ htmlType?: "button" | "submit" | "reset"; /** 배치(마진·그리드) 조정용. 디자인 값 주입은 금지 — 모양이 달라야 하면 `type`을 추가한다. */ className?: string; /** * 아이콘 엘리먼트. 계열별 색은 슬롯이 `color`로 내려주므로 **`currentColor`로 그린 * SVG**를 넣어야 시안대로 물든다(primary는 흰색, secondary는 파랑 …). * 크기는 슬롯이 `form-icon-*` 토큰으로 정하므로 아이콘 자체에 크기를 두지 않아도 된다. */ leadingIcon?: ReactNode; trailingIcon?: ReactNode; label: string; disabled?: boolean; /** 참이면 아무것도 렌더하지 않는다(DOM에 남지 않는다). */ hidden?: boolean; /** 호출부가 소유하는 상태다 — 버튼은 이 값을 읽기만 하고 스스로 켜지 않는다. */ loading?: boolean; fullWidth?: boolean; onAction?: () => void; /** 포인터가 올라오면 `true`, 벗어나면 `false`. */ onHoverChange?: (hovered: boolean) => void; ref?: Ref; } /** * variant마다 클래스를 `Record`로 고정한다 — 계열을 추가하면 여기 항목을 빠뜨릴 수 없다 * (타입 에러). 모양 규칙은 `@fox/styles/_fox-button.scss`가 소유한다. */ const TYPE_CLASS: Record = { primary: styles.typePrimary, secondary: styles.typeSecondary, default: styles.typeDefault, error: styles.typeError, }; const SIZE_CLASS: Record = { lg: styles.sizeLg, md: styles.sizeMd, sm: styles.sizeSm, xsm: styles.sizeXsm, }; /** * @fox 공용 버튼. * * 상태를 갖지 않는 프레젠테이션 컴포넌트다 — `loading`·`disabled`는 호출부가 소유하고 * 버튼은 그에 따라 상호작용을 막기만 한다. 버튼이 스스로 로딩을 켜면 호출부의 상태와 * 어긋날 수 있어 그 책임을 지지 않는다. * * 모양은 Figma 시안(관리자페이지 node 390:439)을 토큰으로만 옮긴 것이며 규칙은 * `@fox/styles/_fox-button.scss`에 있다. `error` 계열만 시안 미정이라 비어 있다. */ export function FoxButton({ type = "default", size, htmlType = "button", className, leadingIcon, trailingIcon, label, disabled = false, hidden = false, loading = false, fullWidth = false, onAction, onHoverChange, ref, }: FoxButtonProps) { if (hidden) { return null; } // 로딩 중에도 클릭을 막아야 같은 요청이 두 번 나가지 않는다. const inactive = disabled || loading; return ( ); }