민혜린 민혜린 08-14
feat: 디자인시스템 FoxRadio, FoxRadioIcon, FoxRadioGruop, FoxConditionalOption 추가
@ca1df31197564578ec7b9dcaedd27168b29a60b3
 
@fox/core/components/fox-conditional-option/fox-conditional-option.tsx (added)
+++ @fox/core/components/fox-conditional-option/fox-conditional-option.tsx
@@ -0,0 +1,129 @@
+"use client";
+
+import { useRef, type ReactNode } from "react";
+import { cx } from "../../utils";
+import { FoxRadio, type FoxRadioSize } from "../fox-radio";
+import { FoxSelect, type FoxSelectItem } from "../fox-select";
+
+export type FoxConditionalOptionOrientation = "horizontal" | "vertical";
+
+export interface FoxConditionalOptionProps {
+  /** 라디오 값. 같은 `name` 안에서 이 칸을 가리킨다. */
+  value: string;
+  label: ReactNode;
+  /** 라벨 아래 보조 설명. 넘기지 않으면 영역을 렌더링하지 않는다. */
+  description?: ReactNode;
+  /** 딸린 select의 목록. */
+  options: FoxSelectItem[];
+  /** 딸린 select의 값(제어). 넘기지 않으면 안에서 들고 있는다. */
+  detail?: string;
+  /** 비제어로 쓸 때 딸린 select의 초기 값. */
+  defaultDetail?: string;
+  /** 딸린 select 값이 바뀔 때 부른다. 값이 첫 인자다. */
+  onChange?: (detail: string) => void;
+  /** 이 칸이 선택될 때 부른다 — select를 골라 자동으로 켜진 경우도 포함이다. */
+  onCheck?: (value: string) => void;
+  /** 아무것도 고르지 않았을 때 select에 보여줄 문구. */
+  placeholder?: ReactNode;
+  orientation?: FoxConditionalOptionOrientation;
+  /** 라디오 묶음 이름. `FoxRadioGroup` 안에 넣으면 그룹이 넣어 주므로 직접 적지 않아도 된다. */
+  name?: string;
+  /** `FoxRadioGroup`이 넣어 준다. 직접 넣으면 그 값이 라디오 크기가 된다. */
+  size?: FoxRadioSize;
+  /** 라디오를 처음부터 선택된 채로 둔다(비제어). */
+  defaultChecked?: boolean;
+  /** 주면 선택 여부를 호출부가 소유한다. */
+  checked?: boolean;
+  disabled?: boolean;
+  /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */
+  hidden?: boolean;
+  /** 배치 조정용. */
+  className?: string;
+}
+
+const ORIENTATION_CLASS: Record<FoxConditionalOptionOrientation, string> = {
+  horizontal: "fox-conditional-option--horizontal",
+  vertical: "fox-conditional-option--vertical",
+};
+
+/**
+ * @fox 후속 질문이 딸린 선택지. 라디오 한 칸과 select를 묶어 하나의 답으로 다룬다.
+ *
+ * select는 이 칸이 골라지지 않아도 **조작할 수 있다.** 값을 고르면 라디오가 자동으로 켜진다 —
+ * 값을 고르고도 라디오를 안 눌러 답이 비는 일이 흔해서다. 반대로 select를 잠가 두면 그 자동
+ * 선택 자체가 불가능해진다(`disabled`는 클릭을 먹지 않는다).
+ *
+ * 켜는 방법은 숨은 input을 실제로 클릭하는 것이다. 그래야 같은 `name`의 다른 칸이 꺼지는
+ * 것과 change 이벤트가 브라우저 규칙 그대로 일어난다 — `checked`를 직접 대입하면 이벤트가
+ * 뛰지 않아 호출부가 모른다.
+ *
+ * `FoxRadioGroup` 안에 그대로 넣을 수 있다 — 그룹이 `name`·`size`를 넣어 준다.
+ *
+ * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"`
+ * (또는 개별 파티셜)로 한 번 불러와야 한다.
+ */
+export function FoxConditionalOption({
+  value,
+  label,
+  description,
+  options,
+  detail,
+  defaultDetail,
+  onChange,
+  onCheck,
+  placeholder,
+  orientation = "vertical",
+  name,
+  size = "lg",
+  defaultChecked,
+  checked,
+  disabled = false,
+  hidden = false,
+  className,
+}: FoxConditionalOptionProps) {
+  const radioRef = useRef<HTMLInputElement>(null);
+
+  if (hidden) {
+    return null;
+  }
+
+  // 비제어일 때의 값은 `FoxSelect`가 이미 들고 있다 — 여기서 또 들면 두 벌이 어긋난다.
+  const handleDetail = (next: string) => {
+    onChange?.(next);
+
+    // 선택 여부는 DOM에서 읽는다 — 형제 칸을 고르면 이 칸이 조용히 꺼지므로 따로 들고 있으면
+    // 어긋난다. 이미 켜져 있으면 다시 클릭해 change를 중복으로 뛰우지 않는다.
+    if (radioRef.current && !radioRef.current.checked) {
+      radioRef.current.click();
+    }
+  };
+
+  return (
+    <div className={cx("fox-conditional-option", ORIENTATION_CLASS[orientation], className)}>
+      <FoxRadio
+        ref={radioRef}
+        name={name}
+        value={value}
+        label={label}
+        description={description}
+        size={size}
+        checked={checked}
+        defaultChecked={defaultChecked}
+        disabled={disabled}
+        onChange={onCheck}
+      />
+
+      <div className="fox-conditional-option__detail">
+        <FoxSelect
+          size="md"
+          options={options}
+          value={detail}
+          defaultValue={defaultDetail}
+          placeholder={placeholder}
+          onValueChange={handleDetail}
+          disabled={disabled}
+        />
+      </div>
+    </div>
+  );
+}
 
@fox/core/components/fox-conditional-option/index.ts (added)
+++ @fox/core/components/fox-conditional-option/index.ts
@@ -0,0 +1,5 @@
+export {
+  FoxConditionalOption,
+  type FoxConditionalOptionProps,
+  type FoxConditionalOptionOrientation,
+} from "./fox-conditional-option";
 
@fox/core/components/fox-radio-group/fox-radio-group.tsx (added)
+++ @fox/core/components/fox-radio-group/fox-radio-group.tsx
@@ -0,0 +1,112 @@
+"use client";
+
+import {
+  Children,
+  Fragment,
+  cloneElement,
+  isValidElement,
+  type ReactElement,
+  type ReactNode,
+  type Ref,
+} from "react";
+import { cx } from "../../utils";
+import type { FoxRadioSize } from "../fox-radio";
+
+export type FoxRadioGroupOrientation = "horizontal" | "vertical";
+
+export interface FoxRadioGroupProps {
+  orientation?: FoxRadioGroupOrientation;
+  /**
+   * 자식 라디오의 `name`을 전부 이 값으로 덮어쓴다. 그룹 안에서 `name`이 어긋나면 브라우저가
+   * 다른 묶음으로 보고 여러 개가 동시에 선택되므로, 그룹이 단일 진실 공급원이다.
+   */
+  name?: string;
+  /** 자식 라디오의 `size`를 전부 이 값으로 덮어쓴다. 넘기지 않으면 자식이 각자 정한다. */
+  size?: FoxRadioSize;
+  /** 그룹을 읽어 줄 이름. 화면에 보이는 제목이 이미 있으면 `labelledBy`를 쓴다. */
+  label?: string;
+  /** 그룹 이름 역할을 하는 요소의 id. `label`보다 우선한다 — 화면의 글자와 어긋나지 않는다. */
+  labelledBy?: string;
+  /** `FoxRadio`들. */
+  children?: ReactNode;
+  /** 배치 조정용. */
+  className?: string;
+  ref?: Ref<HTMLDivElement>;
+}
+
+const ORIENTATION_CLASS: Record<FoxRadioGroupOrientation, string> = {
+  horizontal: "fox-radio-group--horizontal",
+  vertical: "fox-radio-group--vertical",
+};
+
+type Overrides = { name?: string; size?: FoxRadioSize };
+
+/**
+ * 자식 라디오에 그룹 값을 강제로 덮어쓴다.
+ *
+ * - 호스트 요소(`div` 등)는 건드리지 않는다 — DOM에 없는 속성이 넘어가면 React가 경고한다.
+ * - `Children.map`은 프래그먼트 안으로 들어가지 않아 직접 내려간다. 그러지 않으면
+ *   `<>…</>`로 감싼 라디오들이 덮어쓰기를 피해 가고 Fragment에 `name`이 붙어 경고가 난다.
+ */
+function overrideChildren(children: ReactNode, overrides: Overrides): ReactNode {
+  return Children.map(children, (child) => {
+    if (!isValidElement(child)) {
+      return child;
+    }
+
+    if (child.type === Fragment) {
+      const fragment = child as ReactElement<{ children?: ReactNode }>;
+      return overrideChildren(fragment.props.children, overrides);
+    }
+
+    if (typeof child.type === "string") {
+      return child;
+    }
+
+    return cloneElement(child as ReactElement<Record<string, unknown>>, overrides);
+  });
+}
+
+/**
+ * @fox 라디오 묶음. 배치와 `name`·`size` 통일만 책임지고 상태를 갖지 않는다.
+ *
+ * 어느 칸이 선택됐는지는 각 `FoxRadio`의 네이티브 input이 소유한다 — 같은 `name`을 쓰면
+ * 브라우저가 하나만 선택되게 묶고 화살표 이동까지 처리한다.
+ *
+ * `name`은 **그룹이 단일 진실 공급원**이다. 자식마다 적지 않아도 되고, 자식이 다른 값을 줘도
+ * 그룹 값으로 통일되므로 "한 칸만 name을 빼먹어 두 개가 선택되는" 실수가 생기지 않는다.
+ *
+ * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"`
+ * (또는 개별 파티셜)로 한 번 불러와야 한다.
+ */
+export function FoxRadioGroup({
+  orientation = "vertical",
+  name,
+  size,
+  label,
+  labelledBy,
+  children,
+  className,
+  ref,
+}: FoxRadioGroupProps) {
+  // 값을 준 것만 넣는다 — `undefined`를 그대로 넘기면 자식이 스스로 정한 값을 지운다.
+  const overrides: Overrides = {};
+  if (name !== undefined) {
+    overrides.name = name;
+  }
+  if (size !== undefined) {
+    overrides.size = size;
+  }
+
+  return (
+    <div
+      ref={ref}
+      role="radiogroup"
+      aria-label={labelledBy ? undefined : label}
+      aria-labelledby={labelledBy}
+      className={cx("fox-radio-group", ORIENTATION_CLASS[orientation], className)}
+    >
+      {overrideChildren(children, overrides)}
+    </div>
+  );
+}
 
@fox/core/components/fox-radio-group/index.ts (added)
+++ @fox/core/components/fox-radio-group/index.ts
@@ -0,0 +1,5 @@
+export {
+  FoxRadioGroup,
+  type FoxRadioGroupProps,
+  type FoxRadioGroupOrientation,
+} from "./fox-radio-group";
 
@fox/core/components/fox-radio-icon/fox-radio-icon.tsx (added)
+++ @fox/core/components/fox-radio-icon/fox-radio-icon.tsx
@@ -0,0 +1,61 @@
+"use client";
+
+import type { Ref } from "react";
+import { cx } from "../../utils";
+
+export type FoxRadioIconSize = "lg" | "md";
+
+export interface FoxRadioIconProps {
+  size?: FoxRadioIconSize;
+  /**
+   * 선택 상태를 클래스로 못박는다. `FoxRadio` 안에서는 넘기지 않는다 — 네이티브
+   * `<input type="radio">`의 `:checked`가 스타일 훅을 겸하므로 비제어에서도 맞는다.
+   * 상태를 나란히 보여주는 예제처럼 단독으로 쓸 때만 쓴다.
+   */
+  checked?: boolean;
+  /** 비활성 상태를 클래스로 못박는다. `checked`와 같은 이유로 단독 사용 전용이다. */
+  disabled?: boolean;
+  /** 배치 조정용. */
+  className?: string;
+  ref?: Ref<HTMLSpanElement>;
+}
+
+const SIZE_CLASS: Record<FoxRadioIconSize, string> = {
+  lg: "fox-radio-icon--lg",
+  md: "fox-radio-icon--md",
+};
+
+/**
+ * @fox 라디오의 동그란 표시. 상태를 갖지 않는다.
+ *
+ * 자체로는 입력이 아니다 — 실제 선택은 `FoxRadio`가 감싸는 네이티브
+ * `<input type="radio">`가 담당하고, 이 요소는 그 상태를 CSS로 비춘다. 그래서 `FoxRadio`
+ * 안에서는 `checked`·`disabled`를 넘길 필요가 없다.
+ *
+ * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"`
+ * (또는 개별 파티셜)로 한 번 불러와야 한다.
+ */
+export function FoxRadioIcon({
+  size = "lg",
+  checked = false,
+  disabled = false,
+  className,
+  ref,
+}: FoxRadioIconProps) {
+  return (
+    <span
+      ref={ref}
+      aria-hidden="true"
+      className={cx(
+        "fox-radio-icon",
+        SIZE_CLASS[size],
+        checked && "fox-radio-icon--checked",
+        disabled && "fox-radio-icon--disabled",
+        className
+      )}
+    >
+      {/* 선택됐을 때만 넣지 않는다 — 보이기/숨기기는 CSS가 정해야 비제어에서도 맞는다. */}
+      <span className="fox-radio-icon__dot" />
+    </span>
+  );
+}
 
@fox/core/components/fox-radio-icon/index.ts (added)
+++ @fox/core/components/fox-radio-icon/index.ts
@@ -0,0 +1,5 @@
+export {
+  FoxRadioIcon,
+  type FoxRadioIconProps,
+  type FoxRadioIconSize,
+} from "./fox-radio-icon";
 
@fox/core/components/fox-radio/fox-radio.tsx (added)
+++ @fox/core/components/fox-radio/fox-radio.tsx
@@ -0,0 +1,86 @@
+"use client";
+
+import { type ChangeEvent, type ComponentPropsWithRef, type ReactNode } from "react";
+import { cx } from "../../utils";
+import { FoxRadioIcon } from "../fox-radio-icon";
+
+export type FoxRadioSize = "lg" | "md";
+
+/**
+ * 네이티브 `<input type="radio">`가 받는 것을 그대로 넘겨받는다 — name·value·checked·
+ * defaultChecked·disabled·required·form 등. 의미가 겹치는 것만 걷어내고 아래에서 다시 정의한다.
+ */
+type NativeRadioProps = Omit<
+  ComponentPropsWithRef<"input">,
+  "type" | "size" | "className" | "children" | "hidden" | "onChange"
+>;
+
+export interface FoxRadioProps extends NativeRadioProps {
+  size?: FoxRadioSize;
+  label: ReactNode;
+  /** 라벨 아래 보조 설명. 넘기지 않으면 영역을 렌더링하지 않는다. */
+  description?: ReactNode;
+  /**
+   * 이 칸이 선택될 때 부른다. 값이 첫 인자다 — 네이티브 `onChange`를 `Omit`으로 걷어내고
+   * 그 자리를 대신한다(`FoxInput`과 같은 방식).
+   */
+  onChange?: (value: string, event: ChangeEvent<HTMLInputElement>) => void;
+  /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */
+  hidden?: boolean;
+  /** 배치 조정용. */
+  className?: string;
+}
+
+const SIZE_CLASS: Record<FoxRadioSize, string> = {
+  lg: "fox-radio--lg",
+  md: "fox-radio--md",
+};
+
+/**
+ * @fox 라디오 버튼 한 칸. 상태를 갖지 않는다 — 선택 여부는 네이티브
+ * `<input type="radio">`가 소유한다.
+ *
+ * `name`이 같은 라디오끼리는 브라우저가 그룹으로 묶어 화살표 이동·단일 선택·폼 전송을
+ * 처리한다. 그래서 별도 group 컴포넌트를 두지 않는다.
+ *
+ * 루트가 `<label>`이라 라벨이나 설명을 눌러도 선택된다. 동그란 표시는 CSS가 input의
+ * `:checked`·`:disabled`를 보고 그리므로, 비제어로 써도 모양이 어긋나지 않는다.
+ *
+ * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"`
+ * (또는 개별 파티셜)로 한 번 불러와야 한다.
+ */
+export function FoxRadio({
+  size = "lg",
+  label,
+  description,
+  onChange,
+  hidden = false,
+  className,
+  ...rest
+}: FoxRadioProps) {
+  if (hidden) {
+    return null;
+  }
+
+  // 라디오는 켜질 때만 change가 뛴다(꺼질 때는 안 뛴다). 그래도 명시적으로 걸러 두면
+  // 브라우저가 달라져도 "선택될 때"라는 계약이 흔들리지 않는다.
+  const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
+    if (event.target.checked) {
+      onChange?.(event.target.value, event);
+    }
+  };
+
+  return (
+    <label className={cx("fox-radio", SIZE_CLASS[size], className)}>
+      {/* 형제 선택자(`~`)가 뒤따르는 표시와 본문에 닿아야 하므로 맨 앞에 둔다. */}
+      <input {...rest} type="radio" className="fox-radio__input" onChange={handleChange} />
+      <FoxRadioIcon size={size} />
+      <span className="fox-radio__body">
+        <span className="fox-radio__label">{label}</span>
+        {description ? (
+          <span className="fox-radio__description">{description}</span>
+        ) : null}
+      </span>
+    </label>
+  );
+}
 
@fox/core/components/fox-radio/index.ts (added)
+++ @fox/core/components/fox-radio/index.ts
@@ -0,0 +1,1 @@
+export { FoxRadio, type FoxRadioProps, type FoxRadioSize } from "./fox-radio";
@fox/core/components/fox-select/fox-select.tsx
--- @fox/core/components/fox-select/fox-select.tsx
+++ @fox/core/components/fox-select/fox-select.tsx
@@ -37,7 +37,16 @@
    * 호출부마다 `useState`를 반복하는 비용이 크기 때문이다(사용자 승인).
    */
   defaultValue?: string;
-  /** 값이 바뀔 때 호출된다. 이름이 `onChange`가 아닌 이유는 DOM 이벤트가 아니기 때문이다. */
+  /**
+   * 값이 바뀔 때 호출된다.
+   *
+   * ⚠️ 이름을 `onChange`로 바꾸지 않는다. 이 패키지의 기준은 "네이티브 요소를 감싸면
+   * `onChange`(그 네이티브 것을 `Omit`으로 걷어내고 대신한다), 감싸는 요소가 없는 커스텀
+   * 위젯이면 `onValueChange`"다. FoxInput·FoxTextArea·FoxRadio는 앞쪽이고, 이 컴포넌트는
+   * 네이티브 `<select>`가 아니라 버튼 + 리스트박스라 뒤쪽이다.
+   *
+   * 게다가 `FoxEmail`·`FoxPhoneNumber`가 이 이름으로 호출하고 있어 바꾸면 함께 깨진다.
+   */
   onValueChange?: (value: string) => void;
   /** 아무 옵션도 선택되지 않았을 때 트리거에 보여줄 문구. */
   placeholder?: ReactNode;
@fox/core/components/index.ts
--- @fox/core/components/index.ts
+++ @fox/core/components/index.ts
@@ -3,12 +3,16 @@
 export * from "./fox-button-group";
 export * from "./fox-button-panel";
 export * from "./fox-certified";
+export * from "./fox-conditional-option";
 export * from "./fox-email";
 export * from "./fox-file-upload";
 export * from "./fox-icon-button";
 export * from "./fox-input";
 export * from "./fox-link-button";
 export * from "./fox-phone-number";
+export * from "./fox-radio";
+export * from "./fox-radio-group";
+export * from "./fox-radio-icon";
 export * from "./fox-select";
 export * from "./fox-select-option";
 export * from "./fox-select-option-item";
@fox/dev-test/component-registry.tsx
--- @fox/dev-test/component-registry.tsx
+++ @fox/dev-test/component-registry.tsx
@@ -9,11 +9,15 @@
 import { FoxLinkButton } from "../core/components/fox-link-button";
 import { FoxAddress, type FoxAddressSearchResult, type FoxAddressValue } from "../core/components/fox-address";
 import { FoxCertified } from "../core/components/fox-certified";
+import { FoxConditionalOption } from "../core/components/fox-conditional-option";
 import { FoxEmail } from "../core/components/fox-email";
 import { FoxFileUpload, type FoxFileItem } from "../core/components/fox-file-upload";
 import { FoxPhoneNumber, type FoxPhoneNumberState } from "../core/components/fox-phone-number";
 import { FoxTextArea, type FoxTextAreaState } from "../core/components/fox-text-area";
 import { FoxTextButton } from "../core/components/fox-text-button";
+import { FoxRadio } from "../core/components/fox-radio";
+import { FoxRadioGroup } from "../core/components/fox-radio-group";
+import { FoxRadioIcon } from "../core/components/fox-radio-icon";
 import styles from "./dev-test.module.scss";
 
 export interface ComponentExample {
@@ -1006,6 +1010,192 @@
     ],
   },
   {
+    id: "fox-radio-icon",
+    name: "FoxRadioIcon",
+    description:
+      "Figma 시안(radio-icon 107:551) 라디오의 동그란 표시만 담당합니다. SVG가 아니라 CSS로 그립니다 — 시안이 바깥 원과 안쪽 점을 별개 레이어로 두었고, 네 상태가 모양이 아니라 색만 다르기 때문입니다(테두리 두께는 전부 border-1). FoxRadio 안에서는 네이티브 input의 :checked·:disabled가 스타일 훅을 겸해서 클래스가 필요 없고, 아래처럼 단독으로 상태를 비교할 때만 모디파이어로 못박습니다.",
+    variants: [
+      {
+        label: "lg — 기본 / 선택 / 비활성 / 선택+비활성",
+        node: (
+          <>
+            <FoxRadioIcon size="lg" />
+            <FoxRadioIcon size="lg" checked />
+            <FoxRadioIcon size="lg" disabled />
+            <FoxRadioIcon size="lg" checked disabled />
+          </>
+        ),
+      },
+      {
+        label: "md — 기본 / 선택 / 비활성 / 선택+비활성",
+        node: (
+          <>
+            <FoxRadioIcon size="md" />
+            <FoxRadioIcon size="md" checked />
+            <FoxRadioIcon size="md" disabled />
+            <FoxRadioIcon size="md" checked disabled />
+          </>
+        ),
+      },
+    ],
+  },
+  {
+    id: "fox-radio",
+    name: "FoxRadio",
+    description:
+      "Figma 시안(radio 180:278)  동그란 표시 + 라벨 + 부가설명을 묶은 라디오 한 칸입니다.",
+    variants: [
+      {
+        label: "size-lg",
+        node: (
+          <div className={styles.demoStack}>
+            <FoxRadio name="demo-plan" value="basic" label="라디오버튼" size="lg" description="부가적인 설명이 들어갑니다." />
+            <FoxRadio name="demo-plan" value="pro" label="라디오버튼" size="lg" description="부가적인 설명이 들어갑니다." defaultChecked/>
+            <FoxRadio name="demo-plan" value="team" label="라디오버튼" size="lg" description="부가적인 설명이 들어갑니다." disabled />
+            <FoxRadio name="demo-plan" value="team" label="라디오버튼" size="lg" description="부가적인 설명이 들어갑니다." disabled defaultChecked />
+          </div>
+        ),
+      },
+      {
+        label: "size-md",
+        node: (
+          <div className={styles.demoStack}>
+            <FoxRadio name="demo-plan" value="basic" label="라디오버튼" size="md" description="부가적인 설명이 들어갑니다." />
+            <FoxRadio name="demo-plan" value="pro" label="라디오버튼" size="md" description="부가적인 설명이 들어갑니다." defaultChecked/>
+            <FoxRadio name="demo-plan" value="team" label="라디오버튼" size="md" description="부가적인 설명이 들어갑니다." disabled />
+            <FoxRadio name="demo-plan" value="team" label="라디오버튼" size="md" description="부가적인 설명이 들어갑니다." disabled defaultChecked />
+          </div>
+        ),
+      },
+    ],
+  },
+  {
+    id: "fox-radio-group",
+    name: "FoxRadioGroup",
+    description:
+      "Figma 시안(radio-group 194:4327)라디오 여러 칸의 배치와 name·size 통일을 책임집니다. 어느 칸이 선택됐는지는 각 FoxRadio의 네이티브 input이 소유하므로 그룹은 상태를 갖지 않습니다. name은 그룹이 단일 진실 공급원이라 자식마다 적지 않아도 되고, 한 칸만 빼먹어 두 개가 선택되는 실수가 막힙니다. ⚠️ 시안의 변형 이름이 실제 동작과 반대라서(direction=row가 세로로 쌓습니다) 동작 기준으로 다시 이름 붙였습니다 — 시안 row가 vertical, 시안 col이 horizontal입니다.",
+    variants: [
+      {
+        label: "vertical (기본) — 간격 gap-7, 왼쪽 끝 정렬",
+        node: (
+          <FoxRadioGroup name="vertical" label="vertical">
+            <FoxRadio label="라디오버튼" defaultChecked />
+            <FoxRadio  label="라디오버튼" />
+          </FoxRadioGroup>
+        ),
+      },
+      {
+        label: "horizontal — 행 gap-7 / 열 gap-9, 폭이 모자라면 줄바꿈",
+        node: (
+          <FoxRadioGroup orientation="horizontal" name="horizontal" label="horizontal">
+            <FoxRadio label="라디오버튼" defaultChecked />
+            <FoxRadio label="라디오버튼" />
+            <FoxRadio label="라디오버튼" />
+          </FoxRadioGroup>
+        ),
+      },
+      {
+        label: "horizontal + 줄바꿈 — 다음 줄로 넘어간다",
+        node: (
+          <div className={styles.demoNarrow}>
+            <FoxRadioGroup orientation="horizontal" name="rg-wrap" label="줄바꿈 확인">
+              {["첫째", "둘째", "셋째", "넷째", "다섯째"].map((label, i) => (
+                <FoxRadio key={label} value={label} label={label} defaultChecked={i === 0} />
+              ))}
+            </FoxRadioGroup>
+          </div>
+        ),
+      },
+      {
+        label: "size — 자식이 준 값은 무시되고 그룹 값으로 통일된다",
+        node: (
+          <FoxRadioGroup orientation="horizontal" name="rg-md" size="md" label="md 크기">
+            {/* 일부러 어긋난 size를 준다 — 둘 다 md로 나와야 정상 */}
+            <FoxRadio value="a" label="첫째" size="lg" defaultChecked />
+            <FoxRadio value="b" label="둘째" />
+          </FoxRadioGroup>
+        ),
+      },
+    ],
+  },
+    {
+    id: "fox-conditional-option",
+    name: "FoxConditionalOption",
+    description:
+      "Figma 시안(conditional-option 190:2163) 라디오 한 칸과 select가 합쳐 하나의 답을 이룹니다. select는 이 칸이 골라지지 않아도 조작할 수 있고, 값을 고르면 라디오가 자동으로 켜집니다. 잠가 두면 그 자동 선택 자체가 불가능해지기 때문입니다(disabled는 클릭을 먹지 않습니다). 켜는 방법은 숨은 input을 실제로 클릭하는 것이라 형제 칸이 꺼지는 것과 change 이벤트가 브라우저 규칙 그대로 일어납니다. ⚠️ 시안의 변형 이름이 실제 동작과 반대라서(FoxRadioGroup과 같은 문제) 동작 기준으로 다시 이름 붙였습니다 — 시안 row가 vertical, 시안 col이 horizontal입니다.",
+    variants: [
+      {
+        label: "vertical (기본) — select가 아래로 내려가고 라벨 왼쪽 끝에 맞는다",
+        node: (
+          <FoxConditionalOption
+            name="co-v"
+            value="other"
+            label="다른 주소로 배송"
+            description="주소를 고르면 이 항목이 자동으로 선택됩니다."
+            options={SELECT_ITEMS}
+            placeholder="주소를 고르세요"
+          />
+        ),
+      },
+      {
+        label: "horizontal — select가 라디오 옆에 선다",
+        node: (
+          <FoxConditionalOption
+            orientation="horizontal"
+            name="co-h"
+            value="other"
+            label="다른 주소로 배송"
+            options={SELECT_ITEMS}
+            placeholder="주소를 고르세요"
+          />
+        ),
+      },
+      {
+        label: "",
+        node: (
+          <FoxRadioGroup name="co-auto" label="배송지">
+            <FoxRadio value="default" label="기본 배송지" defaultChecked />
+            <FoxConditionalOption
+              value="other"
+              label="다른 주소로 배송"
+              options={SELECT_ITEMS}
+              placeholder="주소를 고르세요"
+            />
+          </FoxRadioGroup>
+        ),
+      },
+      {
+        label: "FoxRadioGroup 안 — name·size를 그룹이 넣어 준다(md로 통일된다)",
+        node: (
+          <FoxRadioGroup name="co-group" size="md" label="결제 수단">
+            <FoxRadio value="card" label="카드" defaultChecked />
+            <FoxConditionalOption
+              value="bank"
+              label="계좌 이체"
+              description="은행을 고르면 이 항목이 선택됩니다."
+              options={SELECT_ITEMS}
+              placeholder="은행을 고르세요"
+            />
+          </FoxRadioGroup>
+        ),
+      },
+      {
+        label: "disabled — 라디오와 select가 함께 잠긴다",
+        node: (
+          <FoxConditionalOption
+            name="co-off"
+            value="other"
+            label="다른 주소로 배송"
+            description="선택할 수 없습니다."
+            options={SELECT_ITEMS}
+            placeholder="주소를 고르세요"
+            disabled
+          />
+        ),
+      },
+    ],
+  },
+  {
     id: "fox-text-area",
     name: "FoxTextArea",
     description:
 
@fox/styles/_fox-conditional-option.scss (added)
+++ @fox/styles/_fox-conditional-option.scss
@@ -0,0 +1,61 @@
+// FoxConditionalOption — 시안: 통합관리자페이지 디자인시스템(KE9UILWhB8qTBe14uXUn4F)
+//                               Figma conditional-option
+//
+// 라디오 한 칸 + 그 칸을 골랐을 때만 의미가 생기는 select를 한 덩어리로 묶는다. 둘이 합쳐
+// 하나의 답이다 — "다른 주소로 배송(라디오) → 어느 주소인지(select)"처럼 후속 질문이 딸린
+// 선택지에 쓴다.
+//
+// 마크업 계약 (React 밖 소비자용):
+//   <div class="fox-conditional-option fox-conditional-option--vertical">
+//     <label class="fox-radio fox-radio--lg">…</label>
+//     <div class="fox-conditional-option__detail">
+//       <div class="fox-select fox-select--md">…</div>
+//     </div>
+//   </div>
+//
+// select는 라디오가 안 골라져 있어도 조작할 수 있다 — 값을 고르면 라디오가 자동으로 켜지는데,
+// 잠가 두면 그 조작 자체가 불가능해진다. 선택 여부는 라디오의 네이티브 input이 소유하므로
+// 이 파일은 배치만 맡는다.
+//
+// ⚠️ 시안의 변형 이름이 실제 동작과 반대다(`fox-radio-group`과 같은 문제). 동작 기준으로
+// 다시 이름 붙였다.
+//
+//   시안 direction=row  →  `--vertical`     (select가 라디오 아래로)
+//   시안 direction=col  →  `--horizontal`   (select가 라디오 옆에)
+//
+// 세로일 때 select는 **라벨 왼쪽 끝**에 맞춘다 — 동그란 표시 폭 + 간격만큼 들여쓴다. 그래야
+// 라벨·부가설명과 한 덩어리로 읽힌다.
+//
+// 토큰이 아닌 값(사용자 승인):
+//   - display / flex / align-*: 배치 규칙.
+
+@use "@fox/styles/abstracts" as fox;
+
+.fox-conditional-option {
+  display: flex;
+
+  // select가 라디오 옆에 선다. 라벨 한 줄과 높이를 맞춘다.
+  &--horizontal {
+    flex-direction: row;
+    align-items: center;
+    gap: fox.gap(5);
+  }
+
+  // select가 라디오 아래로 내려간다.
+  &--vertical {
+    flex-direction: column;
+    align-items: flex-start;
+    gap: fox.gap(4);
+  }
+}
+
+// 들여쓰기 폭은 `fox-radio`의 동그란 표시 폭과 그 간격을 그대로 따른다 — 두 값이 바뀌면
+// 여기도 같이 바뀌어야 라벨 왼쪽 끝에 계속 맞는다. 가로일 때는 옆에 서므로 들여쓰지 않는다.
+.fox-conditional-option--vertical .fox-conditional-option__detail {
+  margin-inline-start: calc(#{fox.form(icon-lg)} + #{fox.gap(3)});
+}
+
+// 라디오가 md면 동그란 표시도 한 단계 작아지므로 들여쓰기도 따라 줄인다.
+.fox-conditional-option--vertical:has(.fox-radio--md) .fox-conditional-option__detail {
+  margin-inline-start: calc(#{fox.form(icon-md)} + #{fox.gap(3)});
+}
 
@fox/styles/_fox-radio-group.scss (added)
+++ @fox/styles/_fox-radio-group.scss
@@ -0,0 +1,57 @@
+// FoxRadioGroup — 시안: 통합관리자페이지 디자인시스템(KE9UILWhB8qTBe14uXUn4F)
+//                        Figma radio-group
+//
+// 라디오 여러 칸의 배치만 책임진다. 선택 상태는 각 `fox-radio`의 네이티브 input이 소유하고,
+// 같은 `name`을 쓰면 브라우저가 하나만 선택되게 묶는다 — 그룹이 상태를 갖지 않는다.
+//
+// 자식들의 `name`을 맞추는 건 작성자 몫이다(React 래퍼는 `name` prop으로 자동화한다). 한 칸만
+// 어긋나도 브라우저가 다른 묶음으로 보고 두 개가 동시에 선택된다.
+//
+// 마크업 계약 (React 밖 소비자용):
+//   <div class="fox-radio-group fox-radio-group--vertical" role="radiogroup" aria-label="요금제">
+//     <label class="fox-radio fox-radio--lg">…</label>
+//     <label class="fox-radio fox-radio--lg">…</label>
+//   </div>
+//
+// ⚠️ 시안의 변형 이름이 실제 동작과 반대다. `direction=row`가 세로로 쌓고
+// (`flex-direction: column`), `direction=col`이 가로로 나열한다(사용자 확인). 이름을 그대로
+// 옮기면 코드가 거짓말을 하므로, **동작 기준으로 다시 이름 붙였다.**
+//
+//   시안 direction=row  →  `--vertical`     (세로로 쌓임)
+//   시안 direction=col  →  `--horizontal`   (가로 나열, 줄바꿈)
+//
+// 단어는 `fox-button-group`의 orientation 규약을 따른다 — 같은 개념을 형제 컴포넌트가 다르게
+// 부르지 않게 한다. 시안과 대조할 때는 위 표를 보면 된다.
+//
+// 가로는 줄바꿈되며 행 간격(`gap-7`)보다 열 간격(`gap-9`)이 넓다. 세로는 한 축뿐이라
+// `gap-7` 하나다.
+//
+// 시안 dev 출력의 `align-self: stretch`(세로)와 `flex: 1 0 0`(가로)은 옮기지 않았다 — 그룹이
+// **부모 안에서** 어떻게 늘어나는지를 정하는 값이라, 규약 7("className은 배치용")에 따라
+// 호출부가 정할 몫이다. 남는 폭을 채워야 하는 자리에서는 `className`으로 준다.
+//
+// 토큰이 아닌 값(사용자 승인):
+//   - display / flex-direction / flex-wrap / align-*: 배치 규칙.
+
+@use "@fox/styles/abstracts" as fox;
+
+.fox-radio-group {
+  display: flex;
+
+  // 세로 — 한 칸씩 아래로 쌓인다. 라벨 길이가 달라도 왼쪽 끝이 맞아야 하므로 flex-start다.
+  &--vertical {
+    flex-direction: column;
+    align-items: flex-start;
+    gap: fox.gap(7);
+  }
+
+  // 가로 — 폭이 모자라면 다음 줄로 넘어간다. `align-content`가 있어야 여러 줄이 됐을 때
+  // 줄 묶음 전체가 가운데 정렬된다.
+  &--horizontal {
+    flex-direction: row;
+    flex-wrap: wrap;
+    align-items: center;
+    align-content: center;
+    gap: fox.gap(7) fox.gap(9);
+  }
+}
 
@fox/styles/_fox-radio-icon.scss (added)
+++ @fox/styles/_fox-radio-icon.scss
@@ -0,0 +1,119 @@
+// FoxRadioIcon — 시안: 통합관리자페이지 디자인시스템(KE9UILWhB8qTBe14uXUn4F)
+//                       Figma radio-icon
+//
+// 라디오의 동그란 표시만 담당한다. 라벨·부가설명까지 묶은 것은 `fox-radio`다.
+//
+// SVG를 쓰지 않고 CSS로 그린다. 시안이 바깥 원과 안쪽 점을 별개 레이어로 두었고 네 상태가
+// 모양이 아니라 색만 다르므로, `border` + `border-radius`로 전부 표현된다. SVG로 가면
+// 배경·테두리 두 색을 `currentColor` 하나로 몰 수 없고 원시 hex가 박힌다.
+//
+// 마크업 계약 (React 밖 소비자용):
+//   <span class="fox-radio-icon fox-radio-icon--lg">
+//     <span class="fox-radio-icon__dot"></span>
+//   </span>
+//
+// 안쪽 점은 **항상 마크업에 둔다.** 선택됐을 때만 넣는 게 아니라 CSS가 보이기/숨기기를
+// 정한다 — 그래야 비제어 입력에서도(React가 선택 여부를 모르는 상태에서도) 모양이 맞는다.
+// 바깥 원이 flex 컨테이너라 점은 저절로 가운데 놓인다.
+//
+// 상태를 거는 방법이 둘이다.
+//   - `fox-radio` 안에서는 네이티브 `<input type="radio">`가 형제로 앞에 있고, 그 요소의
+//     `:checked`·`:disabled`가 그대로 스타일 훅이 된다(규약 3). 클래스를 붙일 필요가 없다.
+//   - 단독으로 쓸 때(예: 상태 비교 예제)는 `--checked`·`--disabled` 모디파이어로 고정한다.
+//
+// 네 상태 모두 테두리 두께는 `border-1`이다 — 선택은 테두리 색만, 비활성은 테두리·배경
+// 색만 바꾼다. 비활성도 기본·선택 두 가지가 있고, 선택된 채 비활성이면 점이 남되 색이
+// 흐려진다.
+//
+// ⚠️ 포커스 표시는 시안에 없다. 네이티브 input을 시각적으로 숨기므로 브라우저 기본 링도
+// 사라져서, 키보드 사용자를 위해 이 파일에서 만들어 준다.
+//
+// 토큰이 아닌 값(사용자 승인):
+//   - md의 안쪽 점 `1rem`(10px): `form-icon-md` 지름의 절반이라 lg(`form-icon-lg` 지름에
+//     `icon-2` 점)과 같은 비율인데, icon 스케일이 `icon-1`에서 `icon-2`로 건너뛰어 그 값에
+//     맞는 토큰이 없다.
+//   - box-sizing / display / flex: 구조 규칙.
+
+@use "@fox/styles/abstracts" as fox;
+
+.fox-radio-icon {
+  // 테두리가 바깥 지름을 늘리지 않게 한다 — 지름은 `form-icon-*`이 정한 값이어야 한다.
+  box-sizing: border-box;
+  display: flex;
+  flex-shrink: 0;
+  align-items: center;
+  justify-content: center;
+  border: fox.border(1) solid fox.color(element-neutral-strong);
+  border-radius: fox.radius(max);
+  background: fox.color(element-neutral-inverse);
+
+  // 안쪽 점. 크기는 크기 모디파이어가, 보이기는 선택 상태가 정한다.
+  &__dot {
+    display: none;
+    flex-shrink: 0;
+    border-radius: fox.radius(max);
+    background: fox.color(element-theme-primary);
+  }
+
+  // ── 크기 ────────────────────────────────────────────────────────────────────
+  // 시안이 바깥 지름에 `form-icon-*`을 건다 — 반응형 토큰이라 모바일에서 한 단계 작아진다.
+  &--lg {
+    inline-size: fox.form(icon-lg);
+    block-size: fox.form(icon-lg);
+  }
+
+  &--lg &__dot {
+    inline-size: fox.icon(2);
+    block-size: fox.icon(2);
+  }
+
+  &--md {
+    inline-size: fox.form(icon-md);
+    block-size: fox.form(icon-md);
+  }
+
+  // 시안 10px — 지름의 1/2로 lg과 같은 비율인데, icon 스케일이 `icon-1`에서 `icon-2`로
+  // 건너뛰어 맞는 토큰이 없다(위 승인 목록 참고).
+  &--md &__dot {
+    inline-size: 1rem;
+    block-size: 1rem;
+  }
+}
+
+// ── 선택 ──────────────────────────────────────────────────────────────────────
+// 두 선택자를 함께 쓴다 — `fox-radio` 안에서는 네이티브 `:checked`가, 단독으로 쓸 때는
+// 모디파이어가 같은 모양을 만든다. 다른 블록(`fox-radio__input`)을 지목하는 것은
+// `fox-button-panel`이 안쪽 `.fox-button`을 지목하는 것과 같은 방식이다.
+.fox-radio-icon--checked,
+.fox-radio__input:checked ~ .fox-radio-icon {
+  border: fox.border(2) solid fox.color(element-theme-primary);
+}
+
+.fox-radio-icon--checked .fox-radio-icon__dot,
+.fox-radio__input:checked ~ .fox-radio-icon .fox-radio-icon__dot {
+  display: block;
+}
+
+// ── 비활성 ────────────────────────────────────────────────────────────────────
+// 선택 규칙보다 뒤에 와야 이긴다 — 비활성이면 선택 여부와 무관하게 바깥 원 색이 같다.
+.fox-radio-icon--disabled,
+.fox-radio__input:disabled ~ .fox-radio-icon {
+  border: fox.border(2) solid fox.color(border-neutral-disabled);
+  background: fox.color(element-neutral-disabled);
+}
+
+// 점은 숨기지 않는다 — 선택된 채 비활성이어도 선택 여부가 보여야 한다. 색만 흐려진다.
+// 보이기/숨기기는 위 선택 규칙이 정하므로 여기서는 색만 건드린다.
+.fox-radio-icon--disabled .fox-radio-icon__dot,
+.fox-radio__input:disabled ~ .fox-radio-icon .fox-radio-icon__dot {
+  background: fox.color(element-neutral-disabled-strong);
+}
+
+// ── 포커스 ────────────────────────────────────────────────────────────────────
+// input이 시각적으로 숨겨져 브라우저 기본 링이 보이지 않으므로 여기서 대신 그린다.
+// 투명이 아니라 실제 링인 이유: 선택 시 테두리도 같은 primary 색이라, 테두리 변화만으로는
+// 포커스와 선택을 구분할 수 없다.
+.fox-radio__input:focus-visible ~ .fox-radio-icon {
+  outline: fox.border(2) solid fox.color(border-theme-primary);
+  outline-offset: fox.border(1);
+}
 
@fox/styles/_fox-radio.scss (added)
+++ @fox/styles/_fox-radio.scss
@@ -0,0 +1,103 @@
+// FoxRadio — 시안: 통합관리자페이지 디자인시스템(KE9UILWhB8qTBe14uXUn4F)
+//                   Figma radio
+//
+// 라디오 버튼 한 칸이다 — 동그란 표시(`fox-radio-icon`) + 라벨 + 부가설명.
+//
+// 마크업 계약 (React 밖 소비자용):
+//   <label class="fox-radio fox-radio--lg">
+//     <input type="radio" class="fox-radio__input" name="plan" value="basic">
+//     <span class="fox-radio-icon fox-radio-icon--lg"></span>
+//     <span class="fox-radio__body">
+//       <span class="fox-radio__label">라벨</span>
+//       <span class="fox-radio__description">부가 설명</span>   <!-- 선택 -->
+//     </span>
+//   </label>
+//
+// 루트가 `<label>`이라 어디를 눌러도 브라우저가 선택을 처리한다. 상태는 전부 네이티브
+// `<input type="radio">`가 소유한다 — `name`이 같으면 그룹·화살표 이동·폼 전송까지 브라우저가
+// 알아서 하므로 별도 group 컴포넌트가 없다(규약 3).
+//
+// input은 순서상 **맨 앞**에 와야 한다. 동그란 표시와 본문이 `~` 형제 선택자로 그 상태를
+// 따라가기 때문이다. 시각적으로만 숨기고 포커스는 살려 둔다 — `display: none`으로 지우면
+// 키보드로 도달할 수 없다.
+//
+// 크기 모디파이어는 안쪽 `fox-radio-icon`의 크기까지 맞춰 준다. 자식에 직접 크기 클래스를
+// 붙이는 것은 React 래퍼가 자동화하지만, 순수 SCSS 소비자는 둘을 같은 단계로 적어야 한다.
+//
+// 토큰이 아닌 값(FoxButton과 동일한 근거로 승인된 항목):
+//   - line-height / letter-spacing: 라벨·부가설명의 시안 텍스트 스타일 값이나 Figma가 변수로
+//     export하지 않는다.
+//
+// 토큰이 아닌 값(사용자 승인):
+//   - cursor / display / flex / position: 구조·상호작용 규칙.
+//   - 숨긴 input의 `inline-size: 0`: 크기가 아니라 "보이지 않게" 하는 구조 값이다.
+
+@use "@fox/styles/abstracts" as fox;
+
+.fox-radio {
+  // 숨긴 input의 위치 기준. 안 주면 input이 조상까지 올라가 엉뚱한 곳에 붙는다.
+  position: relative;
+  display: flex;
+  // 부가설명이 여러 줄이어도 동그란 표시는 첫 줄에 붙어 있어야 한다.
+  align-items: flex-start;
+  gap: fox.gap(3);
+  cursor: pointer;
+
+  // 시각적으로만 숨긴다. `opacity: 0`은 포커스를 유지하므로 키보드로 도달할 수 있고,
+  // 클릭은 루트 `<label>`이 받는다.
+  &__input {
+    position: absolute;
+    inline-size: 0;
+    block-size: 0;
+    margin: 0;
+    opacity: 0;
+    pointer-events: none;
+  }
+
+  &__body {
+    display: flex;
+    flex-direction: column;
+    gap: fox.gap(1);
+    align-items: flex-start;
+  }
+
+  // 자간은 px이 아니라 em이다 — 라벨은 크기 수식어(lg·md)마다, 부가설명은 브레이크포인트마다
+  // 글자 크기가 달라지는데, px으로 박으면 그 자리에서 비율이 어긋난다.
+  &__label {
+    color: fox.color(font-neutral-strong);
+    font-family: fox.font-family(body);
+    font-weight: fox.font-weight(regular);
+    line-height: 1.5;
+    letter-spacing: -0.025em;
+  }
+
+  &__description {
+    color: fox.color(font-neutral-subtle);
+    font-family: fox.font-family(body);
+    font-weight: fox.font-weight(regular);
+    font-size: fox.font-size(body-sm);
+    line-height: 1.5;
+    letter-spacing: -0.025em;
+  }
+
+  // ── 크기 ────────────────────────────────────────────────────────────────────
+  &--lg &__label {
+    font-size: fox.font-size(body-lg);
+  }
+
+  &--md &__label {
+    font-size: fox.font-size(body-md);
+  }
+}
+
+// ── 비활성 ────────────────────────────────────────────────────────────────────
+// 글자색도 함께 흐려진다. 동그란 표시는 `_fox-radio-icon.scss`가 같은 `:disabled`를 보고
+// 처리하므로 여기서 건드리지 않는다.
+.fox-radio:has(.fox-radio__input:disabled) {
+  cursor: default;
+}
+
+.fox-radio__input:disabled ~ .fox-radio__body .fox-radio__label,
+.fox-radio__input:disabled ~ .fox-radio__body .fox-radio__description {
+  color: fox.color(font-neutral-disabled);
+}
@fox/styles/_fox-select-option-item.scss
--- @fox/styles/_fox-select-option-item.scss
+++ @fox/styles/_fox-select-option-item.scss
@@ -43,10 +43,11 @@
   font-family: fox.font-family(body);
   font-weight: fox.font-weight(regular);
   font-size: fox.font-size(body-sm);
-  // line-height 150% / letter-spacing -0.375px — Figma 텍스트 스타일 값이며 변수로
-  // export되지 않아 토큰이 없다(fox-button의 label/*-strong 사례와 같은 예외, 사용자 승인).
+  // line-height 150% / letter-spacing — 시안 텍스트 스타일 값이나 Figma가 변수로
+  // export하지 않는다(사용자 승인). 자간은 px이 아니라 em이라 크기·브레이크포인트가
+  // 바뀌어도 비율이 유지된다.
   line-height: 1.5;
-  letter-spacing: -0.375px;
+  letter-spacing: -0.025em;
   cursor: pointer;
 
   // 키보드 화살표로 이동한 항목도 마우스 호버와 같은 모양이어야 하므로, 실제 `:hover`
@fox/styles/_fox-select.scss
--- @fox/styles/_fox-select.scss
+++ @fox/styles/_fox-select.scss
@@ -132,10 +132,11 @@
     font-family: fox.font-family(body);
     font-weight: fox.font-weight(regular);
     font-size: fox.font-size(body-sm);
-    // line-height 150% / letter-spacing -0.325px — Figma 텍스트 스타일 값이며 변수로
-    // export되지 않아 토큰이 없다(fox-button의 label/*-strong 사례와 같은 예외, 사용자 승인).
+    // line-height 150% / letter-spacing — 시안 텍스트 스타일 값이나 Figma가 변수로
+    // export하지 않는다(사용자 승인). 자간은 px이 아니라 em이라 크기·브레이크포인트가
+    // 바뀌어도 비율이 유지된다.
     line-height: 1.5;
-    letter-spacing: -0.325px;
+    letter-spacing: -0.025em;
   }
 
   // 힌트 색은 트리거의 `aria-invalid`를 따라간다 — 오류 표시를 트리거 한 곳에만 걸면
@fox/styles/components.scss
--- @fox/styles/components.scss
+++ @fox/styles/components.scss
@@ -9,10 +9,14 @@
 @use "fox-button-panel";
 @use "fox-address";
 @use "fox-certified";
+@use "fox-conditional-option";
 @use "fox-email";
 @use "fox-file-upload";
 @use "fox-input";
 @use "fox-phone-number";
+@use "fox-radio";
+@use "fox-radio-group";
+@use "fox-radio-icon";
 @use "fox-select";
 @use "fox-select-option";
 @use "fox-select-option-item";
Add a comment
List