File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
08-14
08-14
08-14
08-18
08-19
08-19
// FoxSegmentedControlItem — 시안: 통합관리자페이지 디자인시스템 Figma segmented-control-item
//
// 세그먼티드 컨트롤의 칸 하나다. 묶는 테두리와 모서리는 `fox-segmented-control`이 갖고,
// 이 파일은 칸 안쪽(높이·여백·글자·아이콘·상태 배경)과 칸 사이 구분선만 책임진다.
//
// 마크업 계약 (React 밖 소비자용):
// <label class="fox-segmented-control-item fox-segmented-control-item--lg">
// <input type="radio" class="fox-segmented-control-item__input" name="view" value="list">
// <span class="fox-segmented-control-item__icon">…svg…</span> <!-- 넘긴 때만 -->
// <span class="fox-segmented-control-item__label">목록</span>
// </label>
//
// 루트가 `<label>`이라 어디를 눌러도 브라우저가 선택을 처리한다. 상태는 전부 네이티브
// `<input type="radio">`가 소유한다 — `name`이 같으면 단일 선택·화살표 이동·폼 전송까지
// 브라우저가 알아서 한다(규약 3, `fox-radio`와 같은 구조).
//
// input은 순서상 **맨 앞**에 와야 한다. 시각적으로만 숨기고 포커스는 살려 둔다 —
// `display: none`으로 지우면 키보드로 도달할 수 없다.
//
// 켜짐·비활성을 `:has()`로 본다. `fox-radio`는 표시가 input의 **형제**라 `~`로 닿지만,
// 여기서는 색이 바뀌는 것이 input을 **감싼 루트**라 형제 선택자로는 닿을 수 없다
// (`fox-checkbox`가 루트 커서를 바꿀 때 쓰는 방식과 같다).
//
// ── 구분선 ────────────────────────────────────────────────────────────────────
// 칸마다 **오른쪽에만** 선을 둔다. 칸 N과 N+1 사이의 선은 N이 갖는다.
//
// ⚠️ 마지막 칸의 선은 지운다(투명). 시안에 없는 처리지만, 두면 묶음 자신의 테두리 바로
// 안쪽에 같은 색 선이 하나 더 생겨 두 줄로 보인다. 지우지 않고 투명으로 두는 이유는 폭이
// 남아 있어야 칸 너비가 위치에 따라 흔들리지 않기 때문이다.
//
// 켜짐·비활성은 선을 갖지 않는다 — 배경이 채워지는 상태라 시안이 `border-right`를 주지 않는다.
//
// ── 상태 ──────────────────────────────────────────────────────────────────────
// ⚠️ hover·pressed는 꺼져 있을 때만 걸린다. 시안이 켜짐 위의 hover를 그리지 않았고, 임의로
// 만들면 채워진 남색이 눌린 채로 흔들린다.
//
// 크기가 바꾸는 것: 높이·좌우 여백·글자 크기·아이콘 크기, 그리고 xsm만 간격(`gap-1`)이다.
//
// 토큰이 아닌 값(`fox-button`·`fox-tag`와 동일한 근거로 승인된 항목):
// - letter-spacing: 시안 텍스트 스타일 label/*-strong의 값이나 Figma가 변수로 export하지
// 않는다. 네 크기가 모두 `-0.025em`이라 규칙 하나로 끝난다(17px→-0.425px,
// 15px→-0.375px, 13px→-0.325px, 11px→-0.275px). px이 아니라 em인 이유는 글자 크기가 크기
// 수식어마다, 그리고 브레이크포인트마다 달라지기 때문이다.
// - 포커스 링의 `outline-offset` 음수: 묶음이 `overflow: hidden`이라 바깥으로 그리면
// 잘린다. 안쪽으로 그려야 첫 칸·마지막 칸에서도 온전히 보인다.
// - box-sizing / position / display / flex / cursor / white-space / aspect-ratio:
// 구조·상호작용 규칙.
@use "@fox/styles/abstracts" as fox;
.fox-segmented-control-item {
// 여백과 구분선이 바깥 크기를 늘리지 않게 한다 — 고정 높이가 흔들리지 않는다.
box-sizing: border-box;
// 숨긴 input의 위치 기준.
position: relative;
display: inline-flex;
justify-content: center;
align-items: center;
border-inline-end: fox.border(1) solid fox.color(button-default-border);
background: fox.color(action-secondary);
cursor: pointer;
color: fox.color(font-neutral-default);
font-family: fox.font-family(body);
font-weight: fox.font-weight(medium);
line-height: 1;
letter-spacing: -0.025em;
text-align: center;
white-space: nowrap;
// 시각적으로만 숨긴다. `opacity: 0`은 포커스를 유지하므로 키보드로 도달할 수 있고,
// 클릭은 루트 `<label>`이 받는다.
&__input {
position: absolute;
inline-size: 0;
block-size: 0;
margin: 0;
opacity: 0;
pointer-events: none;
}
&__icon {
display: inline-flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
// 시안이 아이콘에 정사각 비율을 못박는다. 자리가 좁아 눌려도 글리프가 찌그러지지 않는다.
aspect-ratio: 1 / 1;
}
// 넘긴 SVG가 슬롯을 꽉 채운다 — 크기는 슬롯이 정하고 SVG는 따라온다.
&__icon > * {
inline-size: 100%;
block-size: 100%;
}
// 글자만 줄어들게 해 아이콘이 밀리지 않게 한다.
&__label {
min-inline-size: 0;
}
// ── 크기 ────────────────────────────────────────────────────────────────────
&--lg {
block-size: fox.form(height-lg);
padding-inline: fox.form(padding-lg);
gap: fox.gap(2);
font-size: fox.font-size(label-lg);
}
&--lg &__icon {
inline-size: fox.form(icon-lg);
font-size: fox.form(icon-lg);
block-size: fox.form(icon-lg);
}
&--md {
block-size: fox.form(height-md);
padding-inline: fox.form(padding-md);
gap: fox.gap(2);
font-size: fox.font-size(label-md);
}
&--md &__icon {
inline-size: fox.form(icon-md);
font-size: fox.form(icon-md);
block-size: fox.form(icon-md);
}
&--sm {
block-size: fox.form(height-sm);
padding-inline: fox.form(padding-sm);
gap: fox.gap(2);
font-size: fox.font-size(label-sm);
}
&--sm &__icon {
inline-size: fox.form(icon-sm);
font-size: fox.form(icon-sm);
block-size: fox.form(icon-sm);
}
// xsm만 간격이 한 단계 좁다.
&--xsm {
block-size: fox.form(height-xsm);
padding-inline: fox.form(padding-xsm);
gap: fox.gap(1);
font-size: fox.font-size(label-xsm);
}
&--xsm &__icon {
inline-size: fox.form(icon-xsm);
font-size: fox.form(icon-xsm);
block-size: fox.form(icon-xsm);
}
}
// ── 상태 ──────────────────────────────────────────────────────────────────────
// 순서가 규칙이다: hover·pressed → 켜짐 → 비활성. 뒤에 오는 것이 이긴다. `fox-radio`처럼
// 중첩 밖에 평평하게 적는다 — `:has()` 안에 전체 클래스 이름이 그대로 보여야 읽힌다.
// 꺼져 있고 쓸 수 있을 때만 걸린다(파일 상단 ⚠️ 참고).
.fox-segmented-control-item:not(:has(.fox-segmented-control-item__input:checked)):not(
:has(.fox-segmented-control-item__input:disabled)
):hover {
background: fox.color(action-secondary-hover);
}
// 시안의 pressed는 `:active`가 받는다.
.fox-segmented-control-item:not(:has(.fox-segmented-control-item__input:checked)):not(
:has(.fox-segmented-control-item__input:disabled)
):active {
background: fox.color(action-secondary-pressed);
}
// 켜짐. 글자와 아이콘이 함께 반전색이 된다 — 아이콘은 `currentColor`로 그려야 따라온다.
.fox-segmented-control-item:has(.fox-segmented-control-item__input:checked) {
border-inline-end-color: transparent;
background: fox.color(action-secondary-active);
color: fox.color(font-neutral-static-inverse);
}
.fox-segmented-control-item:has(.fox-segmented-control-item__input:disabled) {
border-inline-end-color: transparent;
background: fox.color(element-neutral-disabled);
color: fox.color(font-neutral-disabled-strong);
cursor: default;
}
// 마지막 칸은 구분선을 지운다(파일 상단 ⚠️ 참고). 켜짐·비활성 규칙과 특이도가 같아 지지
// 않도록 그 뒤에 둔다 — 어차피 셋 다 같은 값이라 누가 이겨도 결과는 같다.
.fox-segmented-control-item:last-child {
border-inline-end-color: transparent;
}
// ── 포커스 ────────────────────────────────────────────────────────────────────
// input이 시각적으로 숨겨져 브라우저 기본 링이 보이지 않으므로 여기서 대신 그린다.
// 안쪽으로 그리는 이유는 파일 상단 승인 목록 참고.
.fox-segmented-control-item:has(.fox-segmented-control-item__input:focus-visible) {
outline: fox.border(2) solid fox.color(border-theme-primary);
outline-offset: calc(-1 * #{fox.border(2)});
}