File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
// FoxButton 스타일 — 시안: 관리자페이지 Figma, node 390:439 (`btn`)
//
// **프레임워크 무관.** 클래스명이 `fox-` 접두사 + BEM이라 전역에 그대로 풀어도 충돌하지
// 않는다. React 없이 SCSS만 쓰는 프로젝트도 이 파일 하나만 `@use`하고 아래 마크업 계약을
// 지키면 동일한 버튼을 얻는다.
//
// <button class="fox-button fox-button--primary fox-button--md">
// <span class="fox-button__icon">…svg…</span> <!-- 선택 -->
// <span class="fox-button__label">버튼</span>
// <span class="fox-button__icon">…svg…</span> <!-- 선택 -->
// </button>
//
// - 비활성은 네이티브 `disabled` 속성으로 표현한다(별도 모디파이어 없음).
// - 로딩은 앞쪽 아이콘 자리에 `fox-button__spinner`를 두고 `disabled`를 함께 건다.
// - 아이콘 SVG는 `currentColor`로 그려야 계열별 색이 적용된다. 크기는 슬롯이 정한다.
//
// **모든 디자인 값은 토큰으로만 지목한다.** 시안의 Figma 변수가 우리 토큰과 1:1로
// 대응하므로 예외가 필요 없었고, 아래 세 가지만 토큰이 아니다(사용자 승인 후 사용):
//
// 1. `line-height: 1` / `letter-spacing: -0.025em`
// 시안 텍스트 스타일 `label/*-strong`의 값이다(lineHeight 1, letterSpacing -2.5%).
// Figma에서 텍스트 스타일로만 존재하고 변수로 export되지 않아 토큰이 없다.
// 네 크기가 모두 같은 값이라 스케일이 아니고 규칙 하나로 끝난다.
// 2. `box-sizing: border-box`
// 높이가 고정인데 secondary·default가 1px 테두리를 가져, 없으면 계열별로 바깥
// 높이가 2px 어긋난다. 디자인 값이 아니라 레이아웃 메커니즘이다.
// 3. `cursor` / `white-space` / `flex-shrink` 등 구조·상호작용 규칙.
@use "abstracts" as fox;
.fox-button {
box-sizing: border-box;
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
cursor: pointer;
// 시안 label/*-strong. 크기별 font-size는 아래 크기 모디파이어가 정한다.
font-family: fox.font-family(body);
font-weight: fox.font-weight(medium);
line-height: 1;
letter-spacing: -0.025em;
white-space: nowrap;
&:disabled {
cursor: default;
}
// 라벨이 길어도 아이콘을 밀어내지 않게 라벨만 줄어들 수 있게 둔다.
&__label {
min-inline-size: 0;
}
// 아이콘·로딩 슬롯. 크기는 크기 모디파이어가, 색은 계열 모디파이어가 정한다.
&__icon,
&__spinner {
display: inline-flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
}
&__icon > * {
inline-size: 100%;
block-size: 100%;
}
&--full-width {
inline-size: 100%;
}
// ── 크기 ──────────────────────────────────────────────────────────────────
// 높이만 고정이고 너비는 내용에 따라 가변이다(시안도 좌우 padding만 지정).
&--lg {
block-size: fox.form(height-lg);
padding-inline: fox.form(padding-lg);
border-radius: fox.form(radius-lg);
gap: fox.gap(2);
font-size: fox.font-size(label-lg);
.fox-button__icon,
.fox-button__spinner {
inline-size: fox.form(icon-lg);
block-size: fox.form(icon-lg);
}
}
&--md {
block-size: fox.form(height-md);
padding-inline: fox.form(padding-md);
border-radius: fox.form(radius-md);
gap: fox.gap(2);
font-size: fox.font-size(label-md);
.fox-button__icon,
.fox-button__spinner {
inline-size: fox.form(icon-md);
block-size: fox.form(icon-md);
}
}
&--sm {
block-size: fox.form(height-sm);
padding-inline: fox.form(padding-sm);
border-radius: fox.form(radius-sm);
gap: fox.gap(2);
font-size: fox.font-size(label-sm);
.fox-button__icon,
.fox-button__spinner {
inline-size: fox.form(icon-sm);
block-size: fox.form(icon-sm);
}
}
// xsm만 gap이 한 단계 좁다(시안 확인).
&--xsm {
block-size: fox.form(height-xsm);
padding-inline: fox.form(padding-xsm);
border-radius: fox.form(radius-xsm);
gap: fox.gap(1);
font-size: fox.font-size(label-xsm);
.fox-button__icon,
.fox-button__spinner {
inline-size: fox.form(icon-xsm);
block-size: fox.form(icon-xsm);
}
}
// ── 계열 ──────────────────────────────────────────────────────────────────
// 아이콘 색은 라벨 색과 별개 토큰으로 지정돼 있어(시안) 각각 지목한다 —
// 지금은 두 값이 같지만 토큰이 갈라지면 시안을 따라가야 한다.
&--primary {
background: fox.color(button-primary-surface);
color: fox.color(button-primary-font);
.fox-button__icon {
color: fox.color(icon-neutral-static-inverse);
}
&:hover:not(:disabled) {
background: fox.color(button-primary-surface-hover);
}
&:active:not(:disabled) {
background: fox.color(button-primary-surface-pressed);
}
}
&--secondary {
border: fox.border(1) solid fox.color(button-secondary-border);
background: fox.color(button-secondary-surface);
color: fox.color(button-secondary-font);
.fox-button__icon {
color: fox.color(icon-theme-primary-strong);
}
&:hover:not(:disabled) {
background: fox.color(button-secondary-surface-hover);
}
&:active:not(:disabled) {
background: fox.color(button-secondary-surface-pressed);
}
}
&--default {
border: fox.border(1) solid fox.color(button-default-border);
background: fox.color(button-default-surface);
color: fox.color(button-default-font);
.fox-button__icon {
color: fox.color(icon-neutral-default);
}
&:hover:not(:disabled) {
background: fox.color(button-default-surface-hover);
}
&:active:not(:disabled) {
background: fox.color(button-default-surface-pressed);
}
}
// ⚠️ error 계열은 시안 미정이라 규칙을 두지 않는다. 확정되면 여기에 채운다.
&--error {
}
}
// ── 비활성 ──────────────────────────────────────────────────────────────────
// 계열과 무관하게 같은 모양이고 **테두리가 없다**(시안에서 disabled-border 미사용).
// 계열 모디파이어보다 특이도가 높아야 이기므로 `.fox-button` 블록과 함께 지목한다.
.fox-button:disabled {
border: none;
background: fox.color(button-disabled-surface);
color: fox.color(button-disabled-font);
.fox-button__icon {
color: fox.color(icon-neutral-disabled-strong);
}
}