// FoxToggleSwitch — 시안: 통합관리자페이지 디자인시스템(KE9UILWhB8qTBe14uXUn4F) Figma toggle-switch // // 켜짐/꺼짐을 바로 바꾸는 스위치다 — 트랙(배경) + 동그라미(thumb) + 글자. // // 마크업 계약 (React 밖 소비자용): // // // // // …X… // …체크… // // // 알림 받기 // // // 루트가 ``이라 글자를 눌러도 바뀐다. 상태는 전부 네이티브 ``가 // 소유한다(규약 3) — `role="switch"`만 얹어 체크박스가 아니라 스위치로 읽히게 한다. // // input은 순서상 **맨 앞**에 와야 한다. 트랙과 글자가 `~` 형제 선택자로 그 상태를 따라간다. // 시각적으로만 숨기고 포커스는 살려 둔다 — `display: none`으로 지우면 키보드로 도달할 수 없다. // // 표시 둘(X·체크)을 **항상 마크업에 둔다.** 켜졌을 때만 넣는 게 아니라 CSS가 보이기/숨기기를 // 정한다 — 그래야 비제어 입력에서도(React가 켜짐 여부를 모르는 상태에서도) 모양이 맞는다. // `fox-check-icon`과 같은 처리다. // // 동그라미는 off일 때 왼쪽, on일 때 오른쪽이다. 트랙이 `display: flex`라 기본이 왼쪽이고, // 켜지면 `margin-inline-start: auto`가 오른쪽으로 밀어낸다 — 이동 거리를 계산해 박지 않으므로 // 트랙 폭이나 동그라미 크기가 바뀌어도 따라온다. 시안에 모션이 없고 `@fox` 전체에 // `transition`을 쓰는 곳이 없어 위치만 바뀐다. // // **표시 색은 언제나 트랙 배경과 같은 값이다** — off는 `element-neutral-default`, on은 // `element-theme-primary`, 비활성은 `element-neutral-disabled`. 흰 동그라미에 트랙 색으로 // 글리프를 파낸 모양이다. 그래서 트랙 규칙이 `background`와 `color`를 항상 함께 주고, 표시는 // `currentColor`로 그려 물려받는다 — 상태별로 색을 두 군데 적지 않는다. // // 크기가 바꾸는 것은 트랙 폭·동그라미 지름·표시 크기·글자 크기 넷이다. 안쪽 여백(`padding-1`)과 // 바깥 간격(`gap-3`)은 두 크기가 같다. // // 트랙 동그라미 표시 글자 // lg 40px 20px 16px label-lg // md 32px 16px 12px label-md // // 트랙 높이는 값으로 주지 않는다 — 여백(2px×2) + 동그라미가 곧 높이다. 그래서 lg 24px, // md 20px이 자동으로 나오고, 동그라미 이동 거리도 (트랙 폭 − 여백 − 동그라미)로 따라온다. // // // ⚠️ 포커스 표시는 시안에 없다. 네이티브 input을 시각적으로 숨기므로 브라우저 기본 링도 // 사라져서, 키보드 사용자를 위해 이 파일에서 만들어 준다(`fox-check-icon`과 같은 처리). // // 토큰이 아닌 값(사용자 승인): // - line-height / letter-spacing: 시안 텍스트 스타일 label/*의 값이나 Figma가 변수로 // export하지 않는다. 두 크기가 같은 `-0.025em`이라 규칙 하나로 끝난다(17px→-0.425px, // 15px→-0.375px). px이 아니라 em인 이유는 글자 크기가 크기 수식어마다, 그리고 // 브레이크포인트마다 달라지기 때문이다 — px으로 박으면 그 자리에서 비율이 어긋난다. // - 트랙 폭 `4rem`·`3.2rem`(40·32px) / 동그라미 `2rem`·`1.6rem`(20·16px): 시안이 이 넷만 // 변수 없이 값으로 준다(표시 크기는 `--icon-3`으로 바인딩되어 있어 `fox.icon(*)`을 쓴다). // 스위치 몸통 치수라 값이 같은 `icon-*`을 끌어다 쓰면 뜻이 어긋나서 값으로 둔다 // (1rem = 10px). // - box-sizing: 안쪽 여백이 트랙 40px 안에 들어가야 한다. 없으면 44px이 된다. // - display / flex / cursor / aspect-ratio: 구조·상호작용 규칙. // - 숨긴 input의 `inline-size: 0`: 크기가 아니라 "보이지 않게" 하는 구조 값이다. // // 동그라미의 안쪽 여백(`padding: 6px 5px`)·`gap: 10px`·`flex-direction: column`은 쓰지 않는다. // Figma auto-layout이 남긴 값이고 **그림에 영향을 주지 않는다** — 실제로 넣어 보고 확인했다. // 안쪽 상자가 10×8px(md는 6×4px)로 표시(16px·12px)보다 작지만 `flex-shrink: 0`이라 표시가 줄지 // 않고, 여백이 좌우·상하 대칭이어서 안쪽 상자의 중심이 동그라미 중심과 같다 — 넘친 만큼 양쪽으로 // 고르게 삐져나와 가운데 정렬과 같은 자리에 놓인다. `gap`은 자식이 하나뿐이라, `flex-direction`도 // 자식이 하나라 각각 계산에 참여하지 않는다. // // 게다가 넣으려면 `box-sizing: border-box`가 딸려 와야 한다 — 이 프로젝트에 전역 리셋이 없어 // 그것 없이 여백만 주면 지름이 안쪽 크기가 되어 동그라미가 30×32px 타원으로 부푼다. 결과가 같고 // 규칙이 다섯 줄 늘어날 뿐이라 가운데 정렬만 남긴다. @use "@fox/styles/abstracts" as fox; .fox-toggle-switch { // 숨긴 input의 위치 기준. 안 주면 input이 조상까지 올라가 엉뚱한 곳에 붙는다. position: relative; display: inline-flex; align-items: center; // 크기와 무관하게 같은 값이다(시안). gap: fox.gap(3); cursor: pointer; // 시각적으로만 숨긴다. `opacity: 0`은 포커스를 유지하므로 키보드로 도달할 수 있고, // 클릭은 루트 ``이 받는다. &__input { position: absolute; inline-size: 0; block-size: 0; margin: 0; opacity: 0; pointer-events: none; } // 트랙. `background`와 `color`를 항상 함께 준다 — `color`는 안쪽 표시가 물려받는 값이다. // 폭은 크기 수식어가 정하고, 높이는 여백 + 동그라미가 정한다. &__track { // 안쪽 여백이 트랙 폭 안에 들어가야 한다. 없으면 44px·36px이 된다. box-sizing: border-box; display: flex; align-items: center; flex-shrink: 0; padding: fox.padding(1); border-radius: fox.radius(max); background: fox.color(element-neutral-default); color: fox.color(element-neutral-default); } // 동그라미. 지름은 크기 수식어가 정한다 — 그 값이 트랙 높이까지 정한다. &__thumb { display: flex; flex-shrink: 0; align-items: center; justify-content: center; border-radius: fox.radius(max); background: fox.color(element-neutral-inverse); } // 표시 둘. 보이기는 상태가, 크기는 크기 수식어가 정한다. 색은 `currentColor`라 트랙이 정한 // 값이다. &__mark-off, &__mark-on { flex-shrink: 0; // 시안이 표시에 정사각 비율을 못박는다. 자리가 좁아 눌려도 글리프가 찌그러지지 않는다. aspect-ratio: 1 / 1; } // 꺼짐이 기본이라 X만 보인다. &__mark-on { display: none; } &__label { color: fox.color(font-neutral-default); font-family: fox.font-family(body); font-weight: fox.font-weight(regular); line-height: 1; letter-spacing: -0.025em; } // ── 크기 ──────────────────────────────────────────────────────────────────── // 트랙 40px · 동그라미 20px(→ 트랙 높이 24px) · 표시 16px. &--lg &__track { inline-size: 4rem; } &--lg &__thumb { inline-size: 2rem; font-size: 2rem; block-size: 2rem; } &--lg &__mark-off, &--lg &__mark-on { inline-size: fox.icon(3); font-size: fox.icon(3); block-size: fox.icon(3); } &--lg &__label { font-size: fox.font-size(label-lg); } // 트랙 32px · 동그라미 16px(→ 트랙 높이 20px) · 표시 12px. &--md &__track { inline-size: 3.2rem; } &--md &__thumb { inline-size: 1.6rem; font-size: 1.6rem; block-size: 1.6rem; } &--md &__mark-off, &--md &__mark-on { inline-size: fox.icon(2); font-size: fox.icon(2); block-size: fox.icon(2); } &--md &__label { font-size: fox.font-size(label-md); } } // ── 켜짐 ────────────────────────────────────────────────────────────────────── .fox-toggle-switch__input:checked ~ .fox-toggle-switch__track { background: fox.color(element-theme-primary); color: fox.color(element-theme-primary); } // 동그라미가 오른쪽으로 간다. .fox-toggle-switch__input:checked ~ .fox-toggle-switch__track .fox-toggle-switch__thumb { margin-inline-start: auto; } .fox-toggle-switch__input:checked ~ .fox-toggle-switch__track .fox-toggle-switch__mark-off { display: none; } .fox-toggle-switch__input:checked ~ .fox-toggle-switch__track .fox-toggle-switch__mark-on { display: block; } // ── 비활성 ──────────────────────────────────────────────────────────────────── // 켜짐 규칙보다 뒤에 와야 이긴다 — 비활성이면 켜짐 여부와 무관하게 트랙·동그라미 색이 같다. // 표시와 동그라미 위치는 건드리지 않아 켜짐 여부가 그대로 보인다. .fox-toggle-switch:has(.fox-toggle-switch__input:disabled) { cursor: default; } .fox-toggle-switch__input:disabled ~ .fox-toggle-switch__track { background: fox.color(element-neutral-disabled); color: fox.color(element-neutral-disabled); } .fox-toggle-switch__input:disabled ~ .fox-toggle-switch__track .fox-toggle-switch__thumb { background: fox.color(element-neutral-disabled-strong); } .fox-toggle-switch__input:disabled ~ .fox-toggle-switch__label { color: fox.color(font-neutral-disabled); } // ── 포커스 ──────────────────────────────────────────────────────────────────── // input이 시각적으로 숨겨져 브라우저 기본 링이 보이지 않으므로 여기서 대신 그린다. .fox-toggle-switch__input:focus-visible ~ .fox-toggle-switch__track { outline: fox.border(2) solid fox.color(border-theme-primary); outline-offset: fox.border(1); }