"use client"; import type { ReactNode, Ref } from "react"; import { cx } from "../../utils"; 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; } /** * 클래스명은 `@fox/styles/_fox-button.scss`의 BEM 이름을 그대로 쓴다 — CSS Modules로 * 감싸지 않는 이유는 이 스타일이 React 밖에서도 쓰여야 하기 때문이다. 이름이 `fox-` * 접두사로 전역 유일해서 스코프가 필요 없다. * * variant마다 `Record`로 고정해 계열을 추가하면 항목 누락이 타입 에러가 되게 한다. */ const TYPE_CLASS: Record = { primary: "fox-button--primary", secondary: "fox-button--secondary", default: "fox-button--default", error: "fox-button--error", }; const SIZE_CLASS: Record = { lg: "fox-button--lg", md: "fox-button--md", sm: "fox-button--sm", xsm: "fox-button--xsm", }; /** * @fox 공용 버튼. * * 상태를 갖지 않는 프레젠테이션 컴포넌트다 — `loading`·`disabled`는 호출부가 소유하고 * 버튼은 그에 따라 상호작용을 막기만 한다. 버튼이 스스로 로딩을 켜면 호출부의 상태와 * 어긋날 수 있어 그 책임을 지지 않는다. * * 모양은 Figma 시안(관리자페이지 node 390:439)을 토큰으로만 옮긴 것이며 규칙은 * `@fox/styles/_fox-button.scss`에 있다. `error` 계열만 시안 미정이라 비어 있다. * * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"` * (또는 `@use "@fox/styles/fox-button"`)로 한 번 불러와야 한다. */ 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 ( ); }