// FoxInput — 시안: 통합관리자페이지 디자인시스템(KE9UILWhB8qTBe14uXUn4F) // Figma input (컴포넌트 세트 135:621) // // 마크업 계약 (React 밖 소비자용): //
// //
// // // …svg… //
//

…

//
// // 상태는 두 갈래다. // - 기본(자동): 브라우저가 판단한다. 포커스는 `:focus-within`, 비활성·읽기전용은 네이티브 // 속성, 오류는 `aria-invalid="true"`. 값이 들어찬 모습(시안의 completed)은 따로 규칙이 // 필요 없다 — 시안에서 default와 completed의 차이가 글자색뿐인데 그건 플레이스홀더와 // 입력값의 네이티브 차이다. // - 고정: 루트에 `data-state`를 주면 그 모양으로 못박힌다. 자동 규칙은 전부 꺼진다. // 카탈로그·스냅샷처럼 상태를 눈으로 비교해야 할 때 쓴다. // // 라벨은 입력 크기와 무관하게 항상 label/md다(시안의 form-label이 공용 컴포넌트라 크기를 // 따라가지 않는다). // // 아이콘 크기 24/20/16은 시안이 변수를 걸지 않았지만 상자의 나머지 치수가 전부 `form/*` // 반응형 토큰이라 아이콘도 `form-icon-*`(PC 24/20/16 · 모바일 20/16/12)에 맞춘다. // // 토큰이 아닌 값(FoxButton과 동일한 근거로 승인된 항목): // - line-height / letter-spacing: 시안 텍스트 스타일의 값이나 Figma가 변수로 export하지 않는다. // - 헬퍼 메시지의 line-height 1.5: 위와 같다. // - box-sizing / cursor / flex / appearance 관련: 구조·상호작용 규칙. @use "abstracts" as fox; .fox-input { display: flex; flex-direction: column; gap: fox.gap(3); inline-size: 100%; &__label { color: fox.color(font-neutral-default); font-family: fox.font-family(body); font-size: fox.font-size(label-md); font-weight: fox.font-weight(medium); line-height: 1; letter-spacing: -0.025em; } &__box { box-sizing: border-box; display: flex; align-items: center; gap: fox.gap(3); inline-size: 100%; background: fox.color(input-surface); border: fox.border(1) solid fox.color(input-border); } &__field { 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); font-weight: fox.font-weight(regular); line-height: 1; letter-spacing: -0.025em; &::placeholder { color: fox.color(font-neutral-subtler); } // 포커스는 상자 테두리가 표시하므로 기본 외곽선을 지운다. &:focus { outline: none; } &:disabled { color: fox.color(font-neutral-disabled-strong); cursor: default; } } &__icon, &__clear { display: inline-flex; flex-shrink: 0; align-items: center; justify-content: center; color: fox.color(icon-neutral-default); } &__icon > *, &__clear > * { inline-size: 100%; block-size: 100%; } &__clear { margin: 0; padding: 0; border: none; background: none; cursor: pointer; } &__message { display: flex; 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; } &__message-icon { display: inline-flex; flex-shrink: 0; align-items: flex-start; padding-block-start: fox.padding(1); > * { inline-size: fox.icon(3); block-size: fox.icon(3); } } // ── 크기 ────────────────────────────────────────────────────────────────── &--lg &__box { block-size: fox.form(height-lg); padding-inline: fox.form(padding-lg); border-radius: fox.form(radius-lg); } &--lg &__field { font-size: fox.font-size(label-lg); } &--lg &__icon, &--lg &__clear { inline-size: fox.form(icon-lg); block-size: fox.form(icon-lg); } &--md &__box { block-size: fox.form(height-md); padding-inline: fox.form(padding-md); border-radius: fox.form(radius-md); } &--md &__field { font-size: fox.font-size(label-md); } &--md &__icon, &--md &__clear { inline-size: fox.form(icon-md); block-size: fox.form(icon-md); } &--sm &__box { block-size: fox.form(height-sm); padding-inline: fox.form(padding-sm); border-radius: fox.form(radius-sm); } &--sm &__field { font-size: fox.font-size(label-sm); } &--sm &__icon, &--sm &__clear { inline-size: fox.form(icon-sm); block-size: fox.form(icon-sm); } // 지우기 버튼은 포커스 중에만 보인다(시안에서 focused 변형에만 있다). &__clear { display: none; } &__box:focus-within &__clear { display: inline-flex; } } // ── 상태 · 자동 ───────────────────────────────────────────────────────────── // 뒤에 오는 규칙이 이긴다: 포커스 < 오류 < 읽기전용 < 비활성. .fox-input:not([data-state]) { .fox-input__box:focus-within { border-width: fox.border(2); border-color: fox.color(input-border-active); } .fox-input__box:has(.fox-input__field[aria-invalid="true"]) { border-width: fox.border(2); border-color: fox.color(input-border-error); } // 비활성도 `:read-only`에 걸리므로 떼어낸다. .fox-input__box:has(.fox-input__field:read-only:not(:disabled)) { border-width: fox.border(1); border-color: fox.color(input-border-disabled); background: fox.color(input-surface-disabled); } .fox-input__box:has(.fox-input__field:disabled) { border-width: fox.border(1); border-color: fox.color(input-border-disabled); background: fox.color(input-surface-disabled); } &:focus-within .fox-input__message { color: fox.color(font-system-information); } &:has(.fox-input__field[aria-invalid="true"]) .fox-input__message { color: fox.color(font-system-danger); } } // ── 상태 · 고정 ───────────────────────────────────────────────────────────── // 시안의 focused·completed·error·view는 값이 들어찬 모습이라 플레이스홀더도 진하게 둔다. .fox-input[data-state="default"] .fox-input__box, .fox-input[data-state="completed"] .fox-input__box { border-width: fox.border(1); border-color: fox.color(input-border); background: fox.color(input-surface); } .fox-input[data-state="focused"] .fox-input__box { border-width: fox.border(2); border-color: fox.color(input-border-active); background: fox.color(input-surface); } .fox-input[data-state="error"] .fox-input__box { border-width: fox.border(2); border-color: fox.color(input-border-error); background: fox.color(input-surface); } .fox-input[data-state="view"] .fox-input__box, .fox-input[data-state="disabled"] .fox-input__box { border-width: fox.border(1); border-color: fox.color(input-border-disabled); background: fox.color(input-surface-disabled); } .fox-input[data-state="completed"] .fox-input__field::placeholder, .fox-input[data-state="focused"] .fox-input__field::placeholder, .fox-input[data-state="error"] .fox-input__field::placeholder, .fox-input[data-state="view"] .fox-input__field::placeholder { color: fox.color(font-neutral-default); } .fox-input[data-state="disabled"] .fox-input__field, .fox-input[data-state="disabled"] .fox-input__field::placeholder { color: fox.color(font-neutral-disabled-strong); } .fox-input[data-state="focused"] .fox-input__message { color: fox.color(font-system-information); } .fox-input[data-state="completed"] .fox-input__message { color: fox.color(font-system-success); } .fox-input[data-state="error"] .fox-input__message { color: fox.color(font-system-danger); } .fox-input[data-state="focused"] .fox-input__clear { display: inline-flex; }