File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
08-14
08-18
08-14
08-14
08-18
08-19
08-19
// 입력 계열 공용 조각.
//
// 시안에서 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;
}