// FoxSegmentedControlItem — 시안: 통합관리자페이지 디자인시스템 Figma segmented-control-item // // 세그먼티드 컨트롤의 칸 하나다. 묶는 테두리와 모서리는 `fox-segmented-control`이 갖고, // 이 파일은 칸 안쪽(높이·여백·글자·아이콘·상태 배경)과 칸 사이 구분선만 책임진다. // // 마크업 계약 (React 밖 소비자용): // // // …svg… // 목록 // // // 루트가 ``이라 어디를 눌러도 브라우저가 선택을 처리한다. 상태는 전부 네이티브 // ``가 소유한다 — `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`은 포커스를 유지하므로 키보드로 도달할 수 있고, // 클릭은 루트 ``이 받는다. &__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); 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); 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); 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); 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)}); }