File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
08-14
08-14
08-18
08-18
08-18
08-14
08-14
08-14
08-18
08-18
08-14
08-18
// 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하지 않는다.
//
// 토큰이 아닌 값(사용자 승인):
// - 아이콘의 위쪽 여백 `0.1rem`(1px): 시안 icon-area의 `padding-top` 값이고 변수로 바인딩
// 되어 있지 않다. 크기·브레이크포인트가 바뀌어도 필요한 보정이 0.75~1.25px 범위라 하나로
// 쓴다(1rem = 10px).
// - 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;
}
// 시안이 아이콘을 감싼 icon-area에 위쪽 여백 1px을 준다. 라벨의 line box(글자 크기 × 1.5)가
// 아이콘보다 조금 높아서, 그만큼 내려야 글자 첫 줄과 시각 중심이 맞는다.
//
// 감싸는 요소를 따로 두지 않고 여백으로 처리한다 — 1px 내리는 것 말고는 하는 일이 없어
// React 밖 소비자에게 마크업을 하나 더 요구할 이유가 없다.
.fox-radio-icon {
margin-block-start: 0.1rem;
}
&__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);
}
// ── 글자 감추기 ───────────────────────────────────────────────────────────────
// `display: none`으로 지우면 접근성 트리에서도 빠져 이름 없는 조작기가 된다. 그래서
// 자리만 없애고 DOM에는 남긴다 — 루트가 `<label>`이라 이 글자가 곧 이름이다.
.fox-radio--hide-label {
gap: 0;
> .fox-radio__body {
position: absolute;
inline-size: fox.border(1);
block-size: fox.border(1);
overflow: hidden;
clip-path: inset(50%);
}
}