File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
// FoxTextArea — 시안: 통합관리자페이지 디자인시스템(KE9UILWhB8qTBe14uXUn4F)
// Figma textarea (135:1655 · 섹션 391:3107)
//
// 마크업 계약 (React 밖 소비자용):
// <div class="fox-text-area">
// <label class="fox-text-area__label" for="…">레이블</label>
// <div class="fox-text-area__box">
// <textarea class="fox-text-area__field" id="…" rows="5" placeholder="…"></textarea>
// </div>
// <div class="fox-text-area__footer">
// <p class="fox-text-area__message">…</p> <!-- 선택 -->
// <p class="fox-text-area__counter">
// <span class="fox-text-area__counter-current">80</span>/100
// </p> <!-- 선택 -->
// </div>
// </div>
//
// 상태는 FoxInput과 같은 두 갈래다.
// - 기본(자동): 포커스는 `:focus-within`, 비활성·읽기전용은 네이티브 속성, 오류는
// `aria-invalid="true"`. 시안의 completed는 default와 글자색만 다른데 그건
// 플레이스홀더와 입력값의 네이티브 차이라 규칙이 필요 없다.
// - 고정: 루트에 `data-state`를 주면 그 모양으로 못박힌다.
//
// 시안에 크기 축이 없다 — 상자 하나뿐이고 높이는 줄 수가 정한다(시안은 5줄).
//
// 토큰이 아닌 값(FoxInput과 동일한 근거로 승인된 항목):
// - line-height(본문·헬퍼 1.5, 라벨·카운터 1) / letter-spacing: 시안 텍스트 스타일의
// 값이나 Figma가 변수로 export하지 않는다.
// - box-sizing / resize / flex 관련: 구조·상호작용 규칙. 시안은 리사이즈 핸들을 보여주지
// 않지만 폭이 흔들리지 않도록 세로만 허용한다.
@use "abstracts" as fox;
.fox-text-area {
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;
gap: fox.gap(3);
inline-size: 100%;
padding-inline: fox.form(padding-md);
padding-block: fox.padding(5);
border: fox.border(1) solid fox.color(input-border);
border-radius: fox.form(radius-lg);
background: fox.color(input-surface);
}
&__field {
flex: 1 0 0;
min-inline-size: 0;
margin: 0;
padding: 0;
border: none;
background: none;
resize: vertical;
color: fox.color(font-neutral-default);
font-family: fox.font-family(body);
font-size: fox.font-size(body-md);
font-weight: fox.font-weight(regular);
line-height: 1.5;
letter-spacing: -0.025em;
&::placeholder {
color: fox.color(font-neutral-subtler);
}
// 포커스는 상자 테두리가 표시하므로 기본 외곽선을 지운다.
&:focus {
outline: none;
}
&:disabled {
color: fox.color(font-neutral-disabled-strong);
cursor: default;
}
}
// 헬퍼는 왼쪽에서 남는 폭을 먹고 카운터는 오른쪽에 붙는다.
&__footer {
display: flex;
gap: fox.gap(5);
align-items: center;
justify-content: flex-end;
inline-size: 100%;
}
&__message {
display: flex;
flex: 1 0 0;
gap: fox.number(3);
align-items: flex-start;
min-inline-size: 0;
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);
}
}
&__counter {
flex-shrink: 0;
margin: 0;
color: fox.color(font-neutral-default);
font-family: fox.font-family(body);
font-size: fox.font-size(label-sm);
font-weight: fox.font-weight(regular);
line-height: 1;
letter-spacing: -0.025em;
text-align: end;
}
// 현재 글자 수만 굵고 색이 다르다.
&__counter-current {
color: fox.color(font-theme-primary);
font-weight: fox.font-weight(bold);
}
}
// ── 상태 · 자동 ─────────────────────────────────────────────────────────────
// 뒤에 오는 규칙이 이긴다: 포커스 < 오류 < 읽기전용 < 비활성.
.fox-text-area:not([data-state]) {
.fox-text-area__box:focus-within {
border-width: fox.border(2);
border-color: fox.color(input-border-active);
}
.fox-text-area__box:has(.fox-text-area__field[aria-invalid="true"]) {
border-width: fox.border(2);
border-color: fox.color(input-border-error);
}
// 비활성도 `:read-only`에 걸리므로 떼어낸다.
.fox-text-area__box:has(.fox-text-area__field:read-only:not(:disabled)) {
border-width: fox.border(1);
border-color: fox.color(input-border-disabled);
background: fox.color(input-surface-disabled);
}
.fox-text-area__box:has(.fox-text-area__field:disabled) {
border-width: fox.border(1);
border-color: fox.color(input-border-disabled);
background: fox.color(input-surface-disabled);
}
&:focus-within .fox-text-area__message {
color: fox.color(font-system-information);
}
&:has(.fox-text-area__field[aria-invalid="true"]) .fox-text-area__message {
color: fox.color(font-system-danger);
}
// 시안의 오류 변형에서만 카운터 현재값이 빨갛다.
&:has(.fox-text-area__field[aria-invalid="true"]) .fox-text-area__counter-current {
color: fox.color(font-system-danger);
}
}
// ── 상태 · 고정 ─────────────────────────────────────────────────────────────
// 시안의 focused·completed·error·view는 값이 들어찬 모습이라 플레이스홀더도 진하게 둔다.
.fox-text-area[data-state="default"] .fox-text-area__box,
.fox-text-area[data-state="completed"] .fox-text-area__box {
border-width: fox.border(1);
border-color: fox.color(input-border);
background: fox.color(input-surface);
}
.fox-text-area[data-state="focused"] .fox-text-area__box {
border-width: fox.border(2);
border-color: fox.color(input-border-active);
background: fox.color(input-surface);
}
.fox-text-area[data-state="error"] .fox-text-area__box {
border-width: fox.border(2);
border-color: fox.color(input-border-error);
background: fox.color(input-surface);
}
.fox-text-area[data-state="view"] .fox-text-area__box,
.fox-text-area[data-state="disabled"] .fox-text-area__box {
border-width: fox.border(1);
border-color: fox.color(input-border-disabled);
background: fox.color(input-surface-disabled);
}
.fox-text-area[data-state="completed"] .fox-text-area__field::placeholder,
.fox-text-area[data-state="focused"] .fox-text-area__field::placeholder,
.fox-text-area[data-state="error"] .fox-text-area__field::placeholder,
.fox-text-area[data-state="view"] .fox-text-area__field::placeholder {
color: fox.color(font-neutral-default);
}
.fox-text-area[data-state="disabled"] .fox-text-area__field,
.fox-text-area[data-state="disabled"] .fox-text-area__field::placeholder {
color: fox.color(font-neutral-disabled-strong);
}
.fox-text-area[data-state="focused"] .fox-text-area__message {
color: fox.color(font-system-information);
}
.fox-text-area[data-state="completed"] .fox-text-area__message {
color: fox.color(font-system-success);
}
.fox-text-area[data-state="error"] .fox-text-area__message {
color: fox.color(font-system-danger);
}
.fox-text-area[data-state="error"] .fox-text-area__counter-current {
color: fox.color(font-system-danger);
}