+++ @fox/core/components/fox-toggle-switch/fox-toggle-switch.tsx
... | ... | @@ -0,0 +1,133 @@ |
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import type { ChangeEvent, ComponentPropsWithRef, ReactNode } from "react"; | |
| 4 | +import { cx } from "../../utils"; | |
| 5 | + | |
| 6 | +export type FoxToggleSwitchSize = "lg" | "md"; | |
| 7 | + | |
| 8 | +/** | |
| 9 | + * 네이티브 `<input type="checkbox">`가 받는 것을 그대로 넘겨받는다 — name·value·checked· | |
| 10 | + * defaultChecked·disabled·required·form 등. 의미가 겹치는 것만 걷어내고 아래에서 다시 정의한다. | |
| 11 | + * `role`도 걷어낸다 — 스위치로 읽히는 것이 이 컴포넌트의 정체라 호출부가 바꿀 값이 아니다. | |
| 12 | + */ | |
| 13 | +type NativeCheckboxProps = Omit< | |
| 14 | + ComponentPropsWithRef<"input">, | |
| 15 | + "type" | "role" | "size" | "className" | "children" | "hidden" | "onChange" | |
| 16 | +>; | |
| 17 | + | |
| 18 | +export interface FoxToggleSwitchProps extends NativeCheckboxProps { | |
| 19 | + size?: FoxToggleSwitchSize; | |
| 20 | + /** | |
| 21 | + * 스위치 옆 글자. 넘기지 않으면 영역을 렌더링하지 않는다. | |
| 22 | + * | |
| 23 | + * ⚠️ 생략하면 스위치를 읽어 줄 이름이 없어진다 — 그때는 `aria-label`을 함께 넘긴다 | |
| 24 | + * (네이티브 속성이라 그대로 전달된다). | |
| 25 | + */ | |
| 26 | + label?: ReactNode; | |
| 27 | + /** | |
| 28 | + * 켜지거나 꺼질 때 부른다. 켜짐 여부가 첫 인자다 — 네이티브 `onChange`를 `Omit`으로 | |
| 29 | + * 걷어내고 그 자리를 대신한다(`FoxCheckbox`와 같은 방식). | |
| 30 | + */ | |
| 31 | + onChange?: (checked: boolean, event: ChangeEvent<HTMLInputElement>) => void; | |
| 32 | + /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */ | |
| 33 | + hidden?: boolean; | |
| 34 | + /** 배치 조정용. */ | |
| 35 | + className?: string; | |
| 36 | +} | |
| 37 | + | |
| 38 | +const SIZE_CLASS: Record<FoxToggleSwitchSize, string> = { | |
| 39 | + lg: "fox-toggle-switch--lg", | |
| 40 | + md: "fox-toggle-switch--md", | |
| 41 | +}; | |
| 42 | + | |
| 43 | +/** | |
| 44 | + * @fox 토글 스위치. 켜짐 여부는 네이티브 `<input type="checkbox">`가 소유한다. | |
| 45 | + * | |
| 46 | + * 체크박스와 마크업은 같지만 `role="switch"`를 얹어 "선택됨"이 아니라 "켜짐/꺼짐"으로 읽히게 | |
| 47 | + * 한다. 폼에도 체크박스처럼 참여한다 — 켜져 있으면 `value`가 함께 전송된다. | |
| 48 | + * | |
| 49 | + * 루트가 `<label>`이라 글자를 눌러도 바뀐다. 동그라미는 꺼짐이면 왼쪽, 켜짐이면 오른쪽이고 | |
| 50 | + * X·체크 표시를 둘 다 그려 두고 CSS가 골라 보여 준다 — 켜졌을 때만 넣으면 비제어 입력에서 | |
| 51 | + * React가 상태를 몰라 어긋난다. | |
| 52 | + * | |
| 53 | + * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"` | |
| 54 | + * (또는 개별 파티셜)로 한 번 불러와야 한다. | |
| 55 | + */ | |
| 56 | +export function FoxToggleSwitch({ | |
| 57 | + size = "lg", | |
| 58 | + label, | |
| 59 | + onChange, | |
| 60 | + hidden = false, | |
| 61 | + className, | |
| 62 | + ...rest | |
| 63 | +}: FoxToggleSwitchProps) { | |
| 64 | + if (hidden) { | |
| 65 | + return null; | |
| 66 | + } | |
| 67 | + | |
| 68 | + // 끌 때도 change가 뛴다. 그래서 켜짐 여부를 그대로 넘긴다. | |
| 69 | + const handleChange = (event: ChangeEvent<HTMLInputElement>) => { | |
| 70 | + onChange?.(event.target.checked, event); | |
| 71 | + }; | |
| 72 | + | |
| 73 | + return ( | |
| 74 | + <label className={cx("fox-toggle-switch", SIZE_CLASS[size], className)}> | |
| 75 | + {/* 형제 선택자(`~`)가 뒤따르는 트랙과 글자에 닿아야 하므로 맨 앞에 둔다. */} | |
| 76 | + <input | |
| 77 | + {...rest} | |
| 78 | + type="checkbox" | |
| 79 | + role="switch" | |
| 80 | + className="fox-toggle-switch__input" | |
| 81 | + onChange={handleChange} | |
| 82 | + /> | |
| 83 | + <span className="fox-toggle-switch__track"> | |
| 84 | + <span className="fox-toggle-switch__thumb"> | |
| 85 | + <XMark /> | |
| 86 | + <CheckMark /> | |
| 87 | + </span> | |
| 88 | + </span> | |
| 89 | + {label === undefined ? null : ( | |
| 90 | + <span className="fox-toggle-switch__label">{label}</span> | |
| 91 | + )} | |
| 92 | + </label> | |
| 93 | + ); | |
| 94 | +} | |
| 95 | + | |
| 96 | +// 시안 아이콘 둘. `fill`을 시안의 색 리터럴(#6D7882·#256EF4·#CDD1D5) 대신 `currentColor`로 | |
| 97 | +// 두면 트랙 규칙이 정한 색을 그대로 물려받는다 — 시안에서 표시 색은 언제나 트랙 배경과 같은 | |
| 98 | +// 값이라, 상태가 셋(꺼짐·켜짐·비활성)이어도 SVG는 한 벌로 끝난다. | |
| 99 | +// `width`·`height`를 쓰지 않는다 — 크기는 CSS가 정한다. | |
| 100 | + | |
| 101 | +// 꺼짐 표시. | |
| 102 | +function XMark() { | |
| 103 | + return ( | |
| 104 | + <svg | |
| 105 | + className="fox-toggle-switch__mark-off" | |
| 106 | + viewBox="0 0 16 16" | |
| 107 | + fill="none" | |
| 108 | + aria-hidden="true" | |
| 109 | + > | |
| 110 | + <path | |
| 111 | + d="M13.0306 11.9694C13.1715 12.1103 13.2507 12.3014 13.2507 12.5006C13.2507 12.6999 13.1715 12.891 13.0306 13.0319C12.8897 13.1728 12.6986 13.2519 12.4994 13.2519C12.3001 13.2519 12.109 13.1728 11.9681 13.0319L8 9.06249L4.03063 13.0306C3.88973 13.1715 3.69864 13.2507 3.49938 13.2507C3.30012 13.2507 3.10902 13.1715 2.96813 13.0306C2.82723 12.8897 2.74808 12.6986 2.74808 12.4994C2.74808 12.3001 2.82723 12.109 2.96813 11.9681L6.9375 7.99999L2.96938 4.03061C2.82848 3.88972 2.74933 3.69862 2.74933 3.49936C2.74933 3.30011 2.82848 3.10901 2.96938 2.96811C3.11027 2.82722 3.30137 2.74806 3.50063 2.74806C3.69989 2.74806 3.89098 2.82722 4.03188 2.96811L8 6.93749L11.9694 2.96749C12.1103 2.82659 12.3014 2.74744 12.5006 2.74744C12.6999 2.74744 12.891 2.82659 13.0319 2.96749C13.1728 3.10838 13.2519 3.29948 13.2519 3.49874C13.2519 3.69799 13.1728 3.88909 13.0319 4.02999L9.0625 7.99999L13.0306 11.9694Z" | |
| 112 | + fill="currentColor" | |
| 113 | + /> | |
| 114 | + </svg> | |
| 115 | + ); | |
| 116 | +} | |
| 117 | + | |
| 118 | +// 켜짐 표시. | |
| 119 | +function CheckMark() { | |
| 120 | + return ( | |
| 121 | + <svg | |
| 122 | + className="fox-toggle-switch__mark-on" | |
| 123 | + viewBox="0 0 16 16" | |
| 124 | + fill="none" | |
| 125 | + aria-hidden="true" | |
| 126 | + > | |
| 127 | + <path | |
| 128 | + d="M14.5306 5.03057L6.53063 13.0306C6.46095 13.1005 6.37816 13.156 6.28699 13.1938C6.19583 13.2317 6.09809 13.2512 5.99938 13.2512C5.90067 13.2512 5.80293 13.2317 5.71176 13.1938C5.6206 13.156 5.53781 13.1005 5.46813 13.0306L1.96813 9.53057C1.89836 9.4608 1.84302 9.37798 1.80527 9.28683C1.76751 9.19568 1.74808 9.09798 1.74808 8.99932C1.74808 8.90066 1.76751 8.80296 1.80527 8.71181C1.84302 8.62066 1.89836 8.53783 1.96813 8.46807C2.03789 8.3983 2.12072 8.34296 2.21187 8.30521C2.30302 8.26745 2.40072 8.24802 2.49938 8.24802C2.59804 8.24802 2.69574 8.26745 2.78689 8.30521C2.87804 8.34296 2.96086 8.3983 3.03063 8.46807L6 11.4374L13.4694 3.96932C13.6103 3.82842 13.8014 3.74927 14.0006 3.74927C14.1999 3.74927 14.391 3.82842 14.5319 3.96932C14.6728 4.11021 14.7519 4.30131 14.7519 4.50057C14.7519 4.69983 14.6728 4.89092 14.5319 5.03182L14.5306 5.03057Z" | |
| 129 | + fill="currentColor" | |
| 130 | + /> | |
| 131 | + </svg> | |
| 132 | + ); | |
| 133 | +} |
+++ @fox/core/components/fox-toggle-switch/index.ts
... | ... | @@ -0,0 +1,5 @@ |
| 1 | +export { | |
| 2 | + FoxToggleSwitch, | |
| 3 | + type FoxToggleSwitchProps, | |
| 4 | + type FoxToggleSwitchSize, | |
| 5 | +} from "./fox-toggle-switch"; |
--- @fox/core/components/index.ts
+++ @fox/core/components/index.ts
... | ... | @@ -24,3 +24,4 @@ |
| 24 | 24 |
export * from "./fox-tag-list"; |
| 25 | 25 |
export * from "./fox-text-area"; |
| 26 | 26 |
export * from "./fox-text-button"; |
| 27 |
+export * from "./fox-toggle-switch"; |
--- @fox/dev-test/component-registry.tsx
+++ @fox/dev-test/component-registry.tsx
... | ... | @@ -24,6 +24,7 @@ |
| 24 | 24 |
import { FoxRadioIcon } from "../core/components/fox-radio-icon";
|
| 25 | 25 |
import { FoxTag } from "../core/components/fox-tag";
|
| 26 | 26 |
import { FoxTagList } from "../core/components/fox-tag-list";
|
| 27 |
+import { FoxToggleSwitch } from "../core/components/fox-toggle-switch";
|
|
| 27 | 28 |
import styles from "./dev-test.module.scss"; |
| 28 | 29 |
|
| 29 | 30 |
export interface ComponentExample {
|
... | ... | @@ -1932,4 +1933,50 @@ |
| 1932 | 1933 |
}, |
| 1933 | 1934 |
], |
| 1934 | 1935 |
}, |
| 1936 |
+ {
|
|
| 1937 |
+ id: "fox-toggle-switch", |
|
| 1938 |
+ name: "FoxToggleSwitch", |
|
| 1939 |
+ description: |
|
| 1940 |
+ "Figma 시안(toggle-switch 134:442) 토글 스위치 입니다. 마크업은 체크박스와 같고 role=\"switch\"만 얹어 선택됨이 아니라 켜짐/꺼짐으로 읽히게 합니다 — 폼에도 체크박스처럼 참여합니다. 루트가 <label>이라 글자를 눌러도 바뀌고, 켜짐 여부는 네이티브 input이 소유하므로 아래 예제는 전부 실제로 눌러서 확인할 수 있습니다. 동그라미는 꺼짐이면 왼쪽·켜짐이면 오른쪽이고, 표시 색은 언제나 트랙 배경과 같은 값이라 흰 동그라미에 트랙 색으로 글리프를 파낸 모양이 됩니다. 크기는 트랙·동그라미·표시·글자를 함께 바꿉니다 — lg는 트랙 40×24, md는 트랙 32×20입니다.", |
|
| 1941 |
+ variants: [ |
|
| 1942 |
+ {
|
|
| 1943 |
+ label: "꺼짐 / 켜짐 — 눌러서 바꿔 보세요", |
|
| 1944 |
+ node: ( |
|
| 1945 |
+ <div className={styles.demoStack}>
|
|
| 1946 |
+ <FoxToggleSwitch name="ts-off" label="알림 받기" /> |
|
| 1947 |
+ <FoxToggleSwitch name="ts-on" label="알림 받기" defaultChecked /> |
|
| 1948 |
+ </div> |
|
| 1949 |
+ ), |
|
| 1950 |
+ }, |
|
| 1951 |
+ {
|
|
| 1952 |
+ label: "크기 — lg 트랙 40×24 / md 트랙 32×20, 꺼짐·켜짐 모두", |
|
| 1953 |
+ node: ( |
|
| 1954 |
+ <div className={styles.demoStack}>
|
|
| 1955 |
+ <FoxToggleSwitch name="ts-lg-off" label="lg 꺼짐" /> |
|
| 1956 |
+ <FoxToggleSwitch name="ts-lg" label="lg 켜짐" defaultChecked /> |
|
| 1957 |
+ <FoxToggleSwitch name="ts-md-off" size="md" label="md 꺼짐" /> |
|
| 1958 |
+ <FoxToggleSwitch name="ts-md" size="md" label="md 켜짐" defaultChecked /> |
|
| 1959 |
+ </div> |
|
| 1960 |
+ ), |
|
| 1961 |
+ }, |
|
| 1962 |
+ {
|
|
| 1963 |
+ label: "label 없음 — 글자 영역을 렌더하지 않는다 (읽어 줄 이름은 aria-label로 준다)", |
|
| 1964 |
+ node: ( |
|
| 1965 |
+ <> |
|
| 1966 |
+ <FoxToggleSwitch name="ts-bare-off" aria-label="알림 받기" /> |
|
| 1967 |
+ <FoxToggleSwitch name="ts-bare-on" aria-label="알림 받기" defaultChecked /> |
|
| 1968 |
+ </> |
|
| 1969 |
+ ), |
|
| 1970 |
+ }, |
|
| 1971 |
+ {
|
|
| 1972 |
+ label: "비활성 — 꺼짐 / 켜짐", |
|
| 1973 |
+ node: ( |
|
| 1974 |
+ <div className={styles.demoStack}>
|
|
| 1975 |
+ <FoxToggleSwitch name="ts-off-dis" label="비활성 + 꺼짐" disabled /> |
|
| 1976 |
+ <FoxToggleSwitch name="ts-on-dis" label="비활성 + 켜짐" disabled defaultChecked /> |
|
| 1977 |
+ </div> |
|
| 1978 |
+ ), |
|
| 1979 |
+ }, |
|
| 1980 |
+ ], |
|
| 1981 |
+ }, |
|
| 1935 | 1982 |
]; |
+++ @fox/styles/_fox-toggle-switch.scss
... | ... | @@ -0,0 +1,229 @@ |
| 1 | +// FoxToggleSwitch — 시안: 통합관리자페이지 디자인시스템(KE9UILWhB8qTBe14uXUn4F) Figma toggle-switch | |
| 2 | +// | |
| 3 | +// 켜짐/꺼짐을 바로 바꾸는 스위치다 — 트랙(배경) + 동그라미(thumb) + 글자. | |
| 4 | +// | |
| 5 | +// 마크업 계약 (React 밖 소비자용): | |
| 6 | +// <label class="fox-toggle-switch fox-toggle-switch--lg"> | |
| 7 | +// <input type="checkbox" role="switch" class="fox-toggle-switch__input" name="alarm"> | |
| 8 | +// <span class="fox-toggle-switch__track"> | |
| 9 | +// <span class="fox-toggle-switch__thumb"> | |
| 10 | +// <svg class="fox-toggle-switch__mark-off">…X…</svg> | |
| 11 | +// <svg class="fox-toggle-switch__mark-on">…체크…</svg> | |
| 12 | +// </span> | |
| 13 | +// </span> | |
| 14 | +// <span class="fox-toggle-switch__label">알림 받기</span> <!-- 선택 --> | |
| 15 | +// </label> | |
| 16 | +// | |
| 17 | +// 루트가 `<label>`이라 글자를 눌러도 바뀐다. 상태는 전부 네이티브 `<input type="checkbox">`가 | |
| 18 | +// 소유한다(규약 3) — `role="switch"`만 얹어 체크박스가 아니라 스위치로 읽히게 한다. | |
| 19 | +// | |
| 20 | +// input은 순서상 **맨 앞**에 와야 한다. 트랙과 글자가 `~` 형제 선택자로 그 상태를 따라간다. | |
| 21 | +// 시각적으로만 숨기고 포커스는 살려 둔다 — `display: none`으로 지우면 키보드로 도달할 수 없다. | |
| 22 | +// | |
| 23 | +// 표시 둘(X·체크)을 **항상 마크업에 둔다.** 켜졌을 때만 넣는 게 아니라 CSS가 보이기/숨기기를 | |
| 24 | +// 정한다 — 그래야 비제어 입력에서도(React가 켜짐 여부를 모르는 상태에서도) 모양이 맞는다. | |
| 25 | +// `fox-check-icon`과 같은 처리다. | |
| 26 | +// | |
| 27 | +// 동그라미는 off일 때 왼쪽, on일 때 오른쪽이다. 트랙이 `display: flex`라 기본이 왼쪽이고, | |
| 28 | +// 켜지면 `margin-inline-start: auto`가 오른쪽으로 밀어낸다 — 이동 거리를 계산해 박지 않으므로 | |
| 29 | +// 트랙 폭이나 동그라미 크기가 바뀌어도 따라온다. 시안에 모션이 없고 `@fox` 전체에 | |
| 30 | +// `transition`을 쓰는 곳이 없어 위치만 바뀐다. | |
| 31 | +// | |
| 32 | +// **표시 색은 언제나 트랙 배경과 같은 값이다** — off는 `element-neutral-default`, on은 | |
| 33 | +// `element-theme-primary`, 비활성은 `element-neutral-disabled`. 흰 동그라미에 트랙 색으로 | |
| 34 | +// 글리프를 파낸 모양이다. 그래서 트랙 규칙이 `background`와 `color`를 항상 함께 주고, 표시는 | |
| 35 | +// `currentColor`로 그려 물려받는다 — 상태별로 색을 두 군데 적지 않는다. | |
| 36 | +// | |
| 37 | +// 크기가 바꾸는 것은 트랙 폭·동그라미 지름·표시 크기·글자 크기 넷이다. 안쪽 여백(`padding-1`)과 | |
| 38 | +// 바깥 간격(`gap-3`)은 두 크기가 같다. | |
| 39 | +// | |
| 40 | +// 트랙 동그라미 표시 글자 | |
| 41 | +// lg 40px 20px 16px label-lg | |
| 42 | +// md 32px 16px 12px label-md | |
| 43 | +// | |
| 44 | +// 트랙 높이는 값으로 주지 않는다 — 여백(2px×2) + 동그라미가 곧 높이다. 그래서 lg 24px, | |
| 45 | +// md 20px이 자동으로 나오고, 동그라미 이동 거리도 (트랙 폭 − 여백 − 동그라미)로 따라온다. | |
| 46 | +// | |
| 47 | +// | |
| 48 | +// ⚠️ 포커스 표시는 시안에 없다. 네이티브 input을 시각적으로 숨기므로 브라우저 기본 링도 | |
| 49 | +// 사라져서, 키보드 사용자를 위해 이 파일에서 만들어 준다(`fox-check-icon`과 같은 처리). | |
| 50 | +// | |
| 51 | +// 토큰이 아닌 값(사용자 승인): | |
| 52 | +// - line-height / letter-spacing: 시안 텍스트 스타일 label/*의 값이나 Figma가 변수로 | |
| 53 | +// export하지 않는다. 두 크기가 같은 `-0.025em`이라 규칙 하나로 끝난다(17px→-0.425px, | |
| 54 | +// 15px→-0.375px). px이 아니라 em인 이유는 글자 크기가 크기 수식어마다, 그리고 | |
| 55 | +// 브레이크포인트마다 달라지기 때문이다 — px으로 박으면 그 자리에서 비율이 어긋난다. | |
| 56 | +// - 트랙 폭 `4rem`·`3.2rem`(40·32px) / 동그라미 `2rem`·`1.6rem`(20·16px): 시안이 이 넷만 | |
| 57 | +// 변수 없이 값으로 준다(표시 크기는 `--icon-3`으로 바인딩되어 있어 `fox.icon(*)`을 쓴다). | |
| 58 | +// 스위치 몸통 치수라 값이 같은 `icon-*`을 끌어다 쓰면 뜻이 어긋나서 값으로 둔다 | |
| 59 | +// (1rem = 10px). | |
| 60 | +// - box-sizing: 안쪽 여백이 트랙 40px 안에 들어가야 한다. 없으면 44px이 된다. | |
| 61 | +// - display / flex / cursor / aspect-ratio: 구조·상호작용 규칙. | |
| 62 | +// - 숨긴 input의 `inline-size: 0`: 크기가 아니라 "보이지 않게" 하는 구조 값이다. | |
| 63 | +// | |
| 64 | +// 동그라미의 안쪽 여백(`padding: 6px 5px`)·`gap: 10px`·`flex-direction: column`은 쓰지 않는다. | |
| 65 | +// Figma auto-layout이 남긴 값이고 **그림에 영향을 주지 않는다** — 실제로 넣어 보고 확인했다. | |
| 66 | +// 안쪽 상자가 10×8px(md는 6×4px)로 표시(16px·12px)보다 작지만 `flex-shrink: 0`이라 표시가 줄지 | |
| 67 | +// 않고, 여백이 좌우·상하 대칭이어서 안쪽 상자의 중심이 동그라미 중심과 같다 — 넘친 만큼 양쪽으로 | |
| 68 | +// 고르게 삐져나와 가운데 정렬과 같은 자리에 놓인다. `gap`은 자식이 하나뿐이라, `flex-direction`도 | |
| 69 | +// 자식이 하나라 각각 계산에 참여하지 않는다. | |
| 70 | +// | |
| 71 | +// 게다가 넣으려면 `box-sizing: border-box`가 딸려 와야 한다 — 이 프로젝트에 전역 리셋이 없어 | |
| 72 | +// 그것 없이 여백만 주면 지름이 안쪽 크기가 되어 동그라미가 30×32px 타원으로 부푼다. 결과가 같고 | |
| 73 | +// 규칙이 다섯 줄 늘어날 뿐이라 가운데 정렬만 남긴다. | |
| 74 | + | |
| 75 | +@use "@fox/styles/abstracts" as fox; | |
| 76 | + | |
| 77 | +.fox-toggle-switch { | |
| 78 | + // 숨긴 input의 위치 기준. 안 주면 input이 조상까지 올라가 엉뚱한 곳에 붙는다. | |
| 79 | + position: relative; | |
| 80 | + display: inline-flex; | |
| 81 | + align-items: center; | |
| 82 | + // 크기와 무관하게 같은 값이다(시안). | |
| 83 | + gap: fox.gap(3); | |
| 84 | + cursor: pointer; | |
| 85 | + | |
| 86 | + // 시각적으로만 숨긴다. `opacity: 0`은 포커스를 유지하므로 키보드로 도달할 수 있고, | |
| 87 | + // 클릭은 루트 `<label>`이 받는다. | |
| 88 | + &__input { | |
| 89 | + position: absolute; | |
| 90 | + inline-size: 0; | |
| 91 | + block-size: 0; | |
| 92 | + margin: 0; | |
| 93 | + opacity: 0; | |
| 94 | + pointer-events: none; | |
| 95 | + } | |
| 96 | + | |
| 97 | + // 트랙. `background`와 `color`를 항상 함께 준다 — `color`는 안쪽 표시가 물려받는 값이다. | |
| 98 | + // 폭은 크기 수식어가 정하고, 높이는 여백 + 동그라미가 정한다. | |
| 99 | + &__track { | |
| 100 | + // 안쪽 여백이 트랙 폭 안에 들어가야 한다. 없으면 44px·36px이 된다. | |
| 101 | + box-sizing: border-box; | |
| 102 | + display: flex; | |
| 103 | + align-items: center; | |
| 104 | + flex-shrink: 0; | |
| 105 | + padding: fox.padding(1); | |
| 106 | + border-radius: fox.radius(max); | |
| 107 | + background: fox.color(element-neutral-default); | |
| 108 | + color: fox.color(element-neutral-default); | |
| 109 | + } | |
| 110 | + | |
| 111 | + // 동그라미. 지름은 크기 수식어가 정한다 — 그 값이 트랙 높이까지 정한다. | |
| 112 | + &__thumb { | |
| 113 | + display: flex; | |
| 114 | + flex-shrink: 0; | |
| 115 | + align-items: center; | |
| 116 | + justify-content: center; | |
| 117 | + border-radius: fox.radius(max); | |
| 118 | + background: fox.color(element-neutral-inverse); | |
| 119 | + } | |
| 120 | + | |
| 121 | + // 표시 둘. 보이기는 상태가, 크기는 크기 수식어가 정한다. 색은 `currentColor`라 트랙이 정한 | |
| 122 | + // 값이다. | |
| 123 | + &__mark-off, | |
| 124 | + &__mark-on { | |
| 125 | + flex-shrink: 0; | |
| 126 | + // 시안이 표시에 정사각 비율을 못박는다. 자리가 좁아 눌려도 글리프가 찌그러지지 않는다. | |
| 127 | + aspect-ratio: 1 / 1; | |
| 128 | + } | |
| 129 | + | |
| 130 | + // 꺼짐이 기본이라 X만 보인다. | |
| 131 | + &__mark-on { | |
| 132 | + display: none; | |
| 133 | + } | |
| 134 | + | |
| 135 | + &__label { | |
| 136 | + color: fox.color(font-neutral-default); | |
| 137 | + font-family: fox.font-family(body); | |
| 138 | + font-weight: fox.font-weight(regular); | |
| 139 | + line-height: 1; | |
| 140 | + letter-spacing: -0.025em; | |
| 141 | + } | |
| 142 | + | |
| 143 | + // ── 크기 ──────────────────────────────────────────────────────────────────── | |
| 144 | + // 트랙 40px · 동그라미 20px(→ 트랙 높이 24px) · 표시 16px. | |
| 145 | + &--lg &__track { | |
| 146 | + inline-size: 4rem; | |
| 147 | + } | |
| 148 | + | |
| 149 | + &--lg &__thumb { | |
| 150 | + inline-size: 2rem; | |
| 151 | + block-size: 2rem; | |
| 152 | + } | |
| 153 | + | |
| 154 | + &--lg &__mark-off, | |
| 155 | + &--lg &__mark-on { | |
| 156 | + inline-size: fox.icon(3); | |
| 157 | + block-size: fox.icon(3); | |
| 158 | + } | |
| 159 | + | |
| 160 | + &--lg &__label { | |
| 161 | + font-size: fox.font-size(label-lg); | |
| 162 | + } | |
| 163 | + | |
| 164 | + // 트랙 32px · 동그라미 16px(→ 트랙 높이 20px) · 표시 12px. | |
| 165 | + &--md &__track { | |
| 166 | + inline-size: 3.2rem; | |
| 167 | + } | |
| 168 | + | |
| 169 | + &--md &__thumb { | |
| 170 | + inline-size: 1.6rem; | |
| 171 | + block-size: 1.6rem; | |
| 172 | + } | |
| 173 | + | |
| 174 | + &--md &__mark-off, | |
| 175 | + &--md &__mark-on { | |
| 176 | + inline-size: fox.icon(2); | |
| 177 | + block-size: fox.icon(2); | |
| 178 | + } | |
| 179 | + | |
| 180 | + &--md &__label { | |
| 181 | + font-size: fox.font-size(label-md); | |
| 182 | + } | |
| 183 | +} | |
| 184 | + | |
| 185 | +// ── 켜짐 ────────────────────────────────────────────────────────────────────── | |
| 186 | +.fox-toggle-switch__input:checked ~ .fox-toggle-switch__track { | |
| 187 | + background: fox.color(element-theme-primary); | |
| 188 | + color: fox.color(element-theme-primary); | |
| 189 | +} | |
| 190 | + | |
| 191 | +// 동그라미가 오른쪽으로 간다. | |
| 192 | +.fox-toggle-switch__input:checked ~ .fox-toggle-switch__track .fox-toggle-switch__thumb { | |
| 193 | + margin-inline-start: auto; | |
| 194 | +} | |
| 195 | + | |
| 196 | +.fox-toggle-switch__input:checked ~ .fox-toggle-switch__track .fox-toggle-switch__mark-off { | |
| 197 | + display: none; | |
| 198 | +} | |
| 199 | + | |
| 200 | +.fox-toggle-switch__input:checked ~ .fox-toggle-switch__track .fox-toggle-switch__mark-on { | |
| 201 | + display: block; | |
| 202 | +} | |
| 203 | + | |
| 204 | +// ── 비활성 ──────────────────────────────────────────────────────────────────── | |
| 205 | +// 켜짐 규칙보다 뒤에 와야 이긴다 — 비활성이면 켜짐 여부와 무관하게 트랙·동그라미 색이 같다. | |
| 206 | +// 표시와 동그라미 위치는 건드리지 않아 켜짐 여부가 그대로 보인다. | |
| 207 | +.fox-toggle-switch:has(.fox-toggle-switch__input:disabled) { | |
| 208 | + cursor: default; | |
| 209 | +} | |
| 210 | + | |
| 211 | +.fox-toggle-switch__input:disabled ~ .fox-toggle-switch__track { | |
| 212 | + background: fox.color(element-neutral-disabled); | |
| 213 | + color: fox.color(element-neutral-disabled); | |
| 214 | +} | |
| 215 | + | |
| 216 | +.fox-toggle-switch__input:disabled ~ .fox-toggle-switch__track .fox-toggle-switch__thumb { | |
| 217 | + background: fox.color(element-neutral-disabled-strong); | |
| 218 | +} | |
| 219 | + | |
| 220 | +.fox-toggle-switch__input:disabled ~ .fox-toggle-switch__label { | |
| 221 | + color: fox.color(font-neutral-disabled); | |
| 222 | +} | |
| 223 | + | |
| 224 | +// ── 포커스 ──────────────────────────────────────────────────────────────────── | |
| 225 | +// input이 시각적으로 숨겨져 브라우저 기본 링이 보이지 않으므로 여기서 대신 그린다. | |
| 226 | +.fox-toggle-switch__input:focus-visible ~ .fox-toggle-switch__track { | |
| 227 | + outline: fox.border(2) solid fox.color(border-theme-primary); | |
| 228 | + outline-offset: fox.border(1); | |
| 229 | +} |
--- @fox/styles/components.scss
+++ @fox/styles/components.scss
... | ... | @@ -26,4 +26,5 @@ |
| 26 | 26 |
@use "fox-select-option-item"; |
| 27 | 27 |
@use "fox-tag"; |
| 28 | 28 |
@use "fox-tag-list"; |
| 29 |
-@use "fox-text-area";(파일 끝에 줄바꿈 문자 없음) |
|
| 29 |
+@use "fox-text-area"; |
|
| 30 |
+@use "fox-toggle-switch";(파일 끝에 줄바꿈 문자 없음) |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?