File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
// FoxInput — 시안: 통합관리자페이지 디자인시스템(KE9UILWhB8qTBe14uXUn4F)
// Figma input (컴포넌트 세트 135:621)
//
// 마크업 계약 (React 밖 소비자용):
// <div class="fox-input fox-input--md">
// <label class="fox-input__label" for="…">레이블</label>
// <div class="fox-input__box">
// <input class="fox-input__field" id="…" placeholder="…">
// <button class="fox-input__clear" type="button">…svg…</button> <!-- 선택 -->
// <span class="fox-input__icon">…svg…</span> <!-- 선택 -->
// </div>
// <p class="fox-input__message">…</p> <!-- 선택 -->
// </div>
//
// 상태는 두 갈래다.
// - 기본(자동): 브라우저가 판단한다. 포커스는 `: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;
}