// 입력 계열 공용 조각. // // 시안에서 helper-message · 입력 상자가 모두 같은 공용 컴포넌트라 // FoxInput · FoxTextArea · FoxPhoneNumber · FoxCertified · FoxEmail · FoxAddress · // FoxFileUpload가 같은 선언을 되풀이했다. 그 되풀이를 여기로 모은다. // // 라벨은 여기 없다 — FoxFormLabel이 컴포넌트로 갖는다. // // 믹스인만 있고 규칙이 없어 @use 해도 CSS가 늘지 않는다 — 부르는 자리에서만 나온다. // 그래서 파티셜을 따로 쓰든 묶음(components.scss)으로 쓰든 출력은 그대로다. @use "abstracts" as fox; /// 비활성·읽기전용 상자. 두 상태의 모양이 같아 한곳에 둔다. @mixin _disabled-box { border-width: fox.border(1); border-color: fox.color(input-border-disabled); background: fox.color(input-surface-disabled); } /// 필드 한 벌의 바깥 틀. 라벨·상자·헬퍼가 세로로 쌓인다. @mixin root($gap: 3) { display: flex; flex-direction: column; gap: fox.gap($gap); inline-size: 100%; } /// 시안 helper-message. 오류·포커스 색은 상태 규칙이 덮는다. /// /// `$display`는 폼 안에서 `flex`이고, 단독으로 쓰는 `FoxHelperText`에서만 /// `inline-flex`다 — 그쪽 시안이 내용만큼만 차지하게 준다. /// /// TODO(폼 통합, 적용 후 이 문단 삭제): 이 믹스인을 부르는 폼 다섯 곳은 앞으로 /// `FoxHelperText`(`.fox-helper-text`)로 옮긴다 — 절차는 `_fox-helper-text.scss` 상단 참고. @mixin message($display: flex) { display: $display; gap: fox.number(3); align-items: flex-start; margin: 0; color: fox.color(font-neutral-subtle); font-family: fox.font-family(body); font-size: fox.font-size(body-sm); font-weight: fox.font-weight(regular); line-height: 1.5; letter-spacing: -0.025em; } /// helper-message 왼쪽 아이콘. 글줄 첫 줄에 맞춰 살짝 내린다. @mixin message-icon { display: inline-flex; flex-shrink: 0; align-items: flex-start; padding-block-start: fox.padding(1); > * { inline-size: fox.icon(3); font-size: fox.icon(3); block-size: fox.icon(3); } } /// 입력 상자 껍데기. 높이·좌우 여백·모서리는 부르는 쪽이 정한다 — FoxInput은 크기 /// 수식어가, 나머지는 md 하나가 정한다. textarea는 세로 가운데 정렬을 쓰지 않으므로 /// `$align: null`로 끈다. @mixin box($gap: 3, $align: center) { box-sizing: border-box; display: flex; align-items: $align; gap: fox.gap($gap); inline-size: 100%; border: fox.border(1) solid fox.color(input-border); background: fox.color(input-surface); } /// 상자 하나짜리 크기(시안에 크기 축이 없는 컴포넌트용). @mixin box-size($size: md) { block-size: fox.form(height-#{$size}); padding-inline: fox.form(padding-#{$size}); border-radius: fox.form(radius-#{$size}); } /// 상자 속 알맹이. 테두리·배경은 상자가 그리므로 여기선 지운다. /// 글자 크기는 FoxInput만 크기 수식어가 정하므로 기본값이 없다. /// /// 뒤에 중첩 규칙이 붙으므로 부르는 쪽이 선언을 더할 때는 `@include`보다 앞에 적는다 — /// 뒤에 적으면 Sass가 같은 선택자를 한 번 더 낸다. /// 자동완성이 칠한 배경·글자색을 디자인 값으로 되돌린다. /// /// 브라우저는 자동완성한 칸에 자기 배경을 칠하는데 `background`로는 덮이지 않는다. 안쪽을 /// 가득 채우는 `inset` 그림자로 가리는 것이 표준 우회다. 글자색도 `color`가 먹지 않아 /// `-webkit-text-fill-color`로만 바꿀 수 있다. @mixin _autofilled { box-shadow: inset 0 0 0 100rem fox.color(input-surface); -webkit-text-fill-color: fox.color(font-neutral-default); caret-color: fox.color(font-neutral-default); } @mixin field($font-size: null, $line-height: 1) { flex: 1 0 0; min-inline-size: 0; margin: 0; padding: 0; border: none; background: none; color: fox.color(font-neutral-default); font-family: fox.font-family(body); @if $font-size { font-size: fox.font-size($font-size); } font-weight: fox.font-weight(regular); line-height: $line-height; letter-spacing: -0.025em; &::placeholder { color: fox.color(font-neutral-subtler); } // 포커스는 상자 테두리가 표시하므로 기본 외곽선을 지운다. &:focus { outline: none; } &:disabled { color: fox.color(font-neutral-disabled-strong); cursor: default; } // 표준 `:autofill`만 쓴다 — 접두사(`:-webkit-autofill`)를 함께 적어도 Lightning CSS가 // browserslist 타깃을 보고 표준형으로 합쳐 버려서 같은 규칙이 두 번 나올 뿐이다. &:autofill { @include _autofilled; } } /// 상태 · 자동. 브라우저가 판단한다 — 포커스는 `:focus-within`, 비활성·읽기전용은 /// 네이티브 속성, 오류는 `aria-invalid="true"`. 뒤에 오는 규칙이 이긴다: /// 포커스 < 오류 < 읽기전용 < 비활성. /// /// $field 알맹이의 element 이름(`__field` · `__segment`). /// $invalid-within `:has()`로 필드 전체가 오류인지 볼 때 찾을 것. 조각을 다른 컴포넌트에 /// 맡기는 경우(FoxPhoneNumber unit)는 알맹이가 없으므로 넓게 본다. /// @content 컴포넌트 고유 규칙. `:not([data-state])` 안에 붙는다. @mixin auto-states($block, $field: "field", $invalid-within: null) { $box: ".#{$block}__box"; $input: ".#{$block}__#{$field}"; $message: ".#{$block}__message"; $within: "#{$input}[aria-invalid=\"true\"]"; @if $invalid-within { $within: $invalid-within; } .#{$block}:not([data-state]) { #{$box}:focus-within { border-width: fox.border(2); border-color: fox.color(input-border-active); } #{$box}:has(#{$input}[aria-invalid="true"]) { border-width: fox.border(2); border-color: fox.color(input-border-error); } // 비활성도 `:read-only`에 걸리므로 떼어낸다. #{$box}:has(#{$input}:read-only:not(:disabled)) { @include _disabled-box; } #{$box}:has(#{$input}:disabled) { @include _disabled-box; } &:focus-within #{$message} { color: fox.color(font-system-information); } &:has(#{$within}) #{$message} { color: fox.color(font-system-danger); } @content; } } /// 상태 · 고정. 루트에 `data-state`를 주면 그 모양으로 못박히고 자동 규칙은 꺼진다. /// 카탈로그·스냅샷처럼 상태를 눈으로 비교해야 할 때 쓴다. /// /// 시안의 focused·completed·error·view는 값이 들어찬 모습이라 플레이스홀더도 진하게 둔다. @mixin fixed-states($block, $field: "field") { $root: ".#{$block}"; $box: ".#{$block}__box"; $input: ".#{$block}__#{$field}"; $message: ".#{$block}__message"; #{$root}[data-state="default"] #{$box}, #{$root}[data-state="completed"] #{$box} { border-width: fox.border(1); border-color: fox.color(input-border); background: fox.color(input-surface); } #{$root}[data-state="focused"] #{$box} { border-width: fox.border(2); border-color: fox.color(input-border-active); background: fox.color(input-surface); } #{$root}[data-state="error"] #{$box} { border-width: fox.border(2); border-color: fox.color(input-border-error); background: fox.color(input-surface); } #{$root}[data-state="view"] #{$box}, #{$root}[data-state="disabled"] #{$box} { @include _disabled-box; } #{$root}[data-state="completed"] #{$input}::placeholder, #{$root}[data-state="focused"] #{$input}::placeholder, #{$root}[data-state="error"] #{$input}::placeholder, #{$root}[data-state="view"] #{$input}::placeholder { color: fox.color(font-neutral-default); } #{$root}[data-state="disabled"] #{$input}, #{$root}[data-state="disabled"] #{$input}::placeholder { color: fox.color(font-neutral-disabled-strong); } #{$root}[data-state="focused"] #{$message} { color: fox.color(font-system-information); } #{$root}[data-state="completed"] #{$message} { color: fox.color(font-system-success); } #{$root}[data-state="error"] #{$message} { color: fox.color(font-system-danger); } @content; }