// ───────────────────────────────────────────────────────────────────────────── // 믹스인 — 여러 선언이 항상 함께 가야 하는 패턴을 한 줄로 묶는다. // // 함수(`_functions.scss`)가 "값 하나"를 강제한다면, 믹스인은 "값들의 조합"을 // 강제한다. 타이포 4종(크기·행간·굵기·자간)이나 감소 모션 분기처럼 하나만 빠져도 // 어긋나는 것들을 화면마다 되풀이해 적지 않게 만드는 것이 목적이다. // ───────────────────────────────────────────────────────────────────────────── @use "sass:map"; @use "tokens"; @use "functions" as fn; /// 타이포 프리셋 적용 — 크기·행간·굵기·자간 4종을 한 번에. /// 개별 스칼라를 따로 고르지 못하게 해서 타이포 조합이 무한히 갈라지는 것을 막는다. @mixin typo($name) { @if not map.has-key(tokens.$typo, $name) { @error "[@fox] 알 수 없는 typo 프리셋: `#{$name}` — 사용 가능한 값: #{map.keys(tokens.$typo)}"; } font-size: var(--fox-typo-#{$name}-size); font-weight: var(--fox-typo-#{$name}-weight); line-height: var(--fox-typo-#{$name}-line-height); letter-spacing: var(--fox-typo-#{$name}-letter-spacing); } /// 지정 브레이크포인트 **이상**에서 적용 (모바일 우선). @mixin media($name) { @media (min-width: fn.bp($name)) { @content; } } /// 지정 브레이크포인트 **미만**에서 적용. /// `media()`와 경계가 겹치지 않도록 0.01rem을 뺀다 — 같은 뷰포트 폭에서 두 블록이 /// 동시에 적용되면 나중 선언이 이기는 미묘한 버그가 생긴다. @mixin media-below($name) { @media (max-width: fn.bp($name) - 0.01rem) { @content; } } /// 트랜지션 — `prefers-reduced-motion: no-preference`로 감싸 감소 모션 환경에서는 /// 아예 선언되지 않게 한다(값을 0으로 만드는 것보다 확실하다). /// @param {List} $properties - 전환할 속성. 여러 개면 괄호로 묶어 전달한다. @mixin transition($properties, $duration: base, $easing: out) { @media (prefers-reduced-motion: no-preference) { transition-property: $properties; transition-duration: fn.duration($duration); transition-timing-function: fn.easing($easing); } } /// 키프레임 애니메이션 — 감소 모션 분기는 `transition()`과 동일하게 내장. @mixin animate($name, $duration: base, $easing: out, $fill: both) { @media (prefers-reduced-motion: no-preference) { animation: $name fn.duration($duration) fn.easing($easing) $fill; } } /// 키보드 포커스 링 — `:focus-visible`만 대상으로 하므로 마우스 클릭 시에는 /// 나타나지 않는다. 모든 상호작용 요소에 반드시 넣는다(접근성). @mixin focus-ring($color: null, $width: 0.2rem, $offset: 0.2rem) { $ring-color: fn.color(primary); @if $color != null { $ring-color: $color; } &:focus-visible { outline: $width solid $ring-color; outline-offset: $offset; } } /// 화면에는 보이지 않지만 스크린리더는 읽는 텍스트. /// `display: none`이나 `visibility: hidden`은 보조기기에서도 사라지므로 쓰지 않는다. @mixin visually-hidden { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; } /// 말줄임 — 1줄이면 `text-overflow`, 여러 줄이면 line-clamp. @mixin truncate($lines: 1) { overflow: hidden; @if $lines == 1 { text-overflow: ellipsis; white-space: nowrap; } @else { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: $lines; } } /// 페이지 콘텐츠 가로 폭 제한 + 좌우 여백. @mixin container($max: xl, $gutter: 4) { width: 100%; max-width: fn.bp($max); margin-inline: auto; padding-inline: fn.space($gutter); }