File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
08-14
08-14
08-18
08-18
08-14
08-19
08-19
08-14
08-14
08-18
08-18
08-14
08-18
08-18
08-19
08-19
08-19
// FoxFileUpload — 시안: 통합관리자페이지 디자인시스템(KE9UILWhB8qTBe14uXUn4F)
// file-upload (391:3677) · file-upload-area (392:5968) · file-upload-img (391:3732)
// file-list (392:5938) · file-list-item (391:3509)
//
// 세 모드가 한 컴포넌트다.
// - default: [읽기 전용 입력 + 파일선택(secondary)] — 목록을 붙일 수 있다.
// - area: 점선 상자에 안내문과 파일선택(primary) — 목록을 붙일 수 있고 끌어다 놓기가
// 항상 켜져 있다(안내문이 그렇게 말한다).
// - image: 썸네일 + 파일선택(secondary) + 설명 — 목록이 없다.
//
// 시안에서 버튼·입력이 모두 기존 컴포넌트의 인스턴스라 그대로 조합한다.
// 파일선택 = FoxButton(secondary/primary md) · 삭제 = FoxTextButton(md secondary)
// 썸네일 삭제 = FoxIconButton(overlay xsm — 24px·radius 4·배경흐림·흰 아이콘이 시안과 같다)
// default 모드 입력칸 = FoxInput(md, 읽기 전용)
// 그래서 이 파일은 배치와 목록·썸네일·점선 상자만 갖는다.
//
// 아이콘 색은 에셋에서 확인해 토큰과 맞췄다 — 클립 #8A949E(icon-neutral-subtler),
// 오류 #BD2C0F(icon-system-danger-strong), 빈 썸네일은 검정 25%(icon-neutral-fill).
// 버튼 안 아이콘은 FoxButton·FoxTextButton이 `currentColor`로 칠하므로 여기서 정하지 않는다.
//
// 토큰이 아닌 값:
// - 입력칸 폭과 썸네일 크기는 인라인 스타일로 들어온다 — 시안 값(320px · 213×120px)이
// 토큰에 없어 prop 기본값으로 두었다(사용자 승인).
// - line-height / letter-spacing: 시안 텍스트 스타일의 값이나 Figma가 변수로
// export하지 않는다.
// - object-fit / overflow / position: 구조 규칙.
// - 목록의 margin/padding/list-style 0: `<ul>`의 브라우저 기본값 지우기.
@use "abstracts" as fox;
@use "form-field" as field;
.fox-file-upload {
@include field.root;
// area 모드는 상자와 목록 사이가 한 단계 넓다.
&--area {
gap: fox.gap(7);
}
&__description {
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;
}
// ── default 모드 ──────────────────────────────────────────────────────────
&__row {
display: flex;
align-items: flex-start;
gap: fox.gap(3);
inline-size: 100%;
}
// 폭은 prop이 인라인 스타일로 정한다.
&__field {
flex-shrink: 0;
}
// ── area 모드 ─────────────────────────────────────────────────────────────
&__area {
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
gap: fox.gap(7);
inline-size: 100%;
padding: fox.padding(10);
border: fox.border(2) dashed fox.color(border-neutral-subtle);
border-radius: fox.radius(6);
background: fox.color(surface-neutral-gray);
}
// 끌어오는 동안. 시안에 없는 상태라 기존 토큰으로 최소한만 표시한다.
&__area--over {
border-color: fox.color(border-theme-primary);
background: fox.color(surface-theme-primary);
}
&__area-text {
margin: 0;
inline-size: 100%;
color: fox.color(font-neutral-default);
font-family: fox.font-family(body);
font-size: fox.font-size(body-lg);
font-weight: fox.font-weight(regular);
line-height: 1.5;
letter-spacing: -0.025em;
text-align: center;
}
// ── image 모드 ────────────────────────────────────────────────────────────
&__image {
display: flex;
align-items: center;
gap: fox.gap(6);
inline-size: 100%;
}
&__thumb {
position: relative;
box-sizing: border-box;
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
overflow: hidden;
border: fox.border(1) dashed fox.color(border-neutral-subtle);
border-radius: fox.radius(3);
background: fox.color(surface-neutral-gray);
color: fox.color(icon-neutral-fill);
}
// 그림이 들어오면 점선이 실선이 되고 바탕은 그림이 덮는다.
&__thumb--filled {
border-style: solid;
background: none;
}
&__thumb-icon > * {
inline-size: fox.icon(6);
block-size: fox.icon(6);
}
// 잘라내지 않고 상자 안에 다 보이게 담는다 — 고른 그림이 어떻게 생겼는지 확인하는
// 자리라 일부만 보이면 확인이 되지 않는다.
&__thumb-image {
inline-size: 100%;
block-size: 100%;
object-fit: contain;
}
&__thumb-remove {
position: absolute;
inset-block-start: 50%;
inset-inline-start: 50%;
transform: translate(-50%, -50%);
}
// 버튼은 글자만큼만 차지한다(시안 items-start). 세로 정렬을 stretch로 두면 버튼이
// 칸 끝까지 늘어난다.
&__image-side {
display: flex;
flex: 1 0 0;
flex-direction: column;
align-items: flex-start;
gap: fox.gap(4);
min-inline-size: 0;
}
// 설명만 폭을 채운다(시안 min-w-full) — 위 정렬 때문에 그냥 두면 글자만큼만 남는다.
&__image-side &__description {
inline-size: 100%;
}
// ── 목록 ──────────────────────────────────────────────────────────────────
// 줄이 많아지면 상자 안에서만 스크롤한다.
// `<ul>`이라 브라우저 기본 여백·글머리표를 먼저 지운다 — 안 지우면 왼쪽에 40px이 붙는다.
&__list {
display: flex;
flex-direction: column;
inline-size: 100%;
max-block-size: 32rem;
margin: 0;
padding: 0;
overflow-x: clip;
overflow-y: auto;
border: fox.border(1) solid fox.color(border-neutral-subtle);
border-radius: fox.radius(3);
list-style: none;
}
&__item {
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: fox.gap(4);
justify-content: center;
inline-size: 100%;
padding-inline: fox.padding(6);
padding-block: fox.padding(4);
border-block-end: fox.border(1) solid fox.color(border-neutral-subtle);
background: fox.color(surface-neutral-default);
}
// 오류 줄은 네 변 모두 굵은 테두리를 갖는다.
&__item--error {
border: fox.border(2) solid fox.color(border-system-danger);
background: fox.color(surface-system-danger);
}
&__item-main {
display: flex;
align-items: center;
gap: fox.gap(3);
inline-size: 100%;
}
&__item-title {
display: flex;
flex: 1 0 0;
align-items: center;
gap: fox.gap(2);
min-inline-size: 0;
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;
white-space: nowrap;
}
&__item-icon {
display: inline-flex;
flex-shrink: 0;
color: fox.color(icon-neutral-subtler);
> * {
inline-size: fox.icon(4);
block-size: fox.icon(4);
}
}
// 이름만 줄어들고 확장자·용량은 그대로 남는다.
&__item-name {
overflow: hidden;
text-overflow: ellipsis;
}
&__item-meta {
flex-shrink: 0;
}
&__item-actions {
display: flex;
flex-shrink: 0;
align-items: center;
gap: fox.gap(4);
}
// 오류 문구는 줄 안에서 옅은 선으로 한 번 끊고 붙는다.
&__item-message {
display: flex;
gap: fox.number(3);
align-items: flex-start;
inline-size: 100%;
margin: 0;
padding-block-start: fox.padding(5);
border-block-start: fox.border(1) solid fox.color(border-neutral-overlay);
color: fox.color(font-system-danger);
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;
}
&__item-message-icon {
display: inline-flex;
flex-shrink: 0;
align-items: flex-start;
padding-block-start: fox.padding(1);
color: fox.color(icon-system-danger-strong);
> * {
inline-size: fox.icon(3);
block-size: fox.icon(3);
}
}
}
// 칸은 손으로 고칠 수 없으므로 언제나 읽기 전용이지만, 시안은 비었을 때 흰 상자를 보여준다
// (파일이 들어오면 읽기 전용 본래의 회색 상자가 그대로 나온다 — 시안 upload=true).
// FoxInput의 읽기 전용 규칙이 특이도 0,6,0이라 그보다 높게 적어 로드 순서와 무관하게 이긴다.
.fox-file-upload
.fox-file-upload__field--empty
.fox-input:not([data-state])
.fox-input__box:has(.fox-file-upload__field--empty .fox-input__field:read-only:not(:disabled)),
.fox-file-upload
.fox-file-upload__field--empty
.fox-input:not([data-state])
.fox-input__box:has(.fox-input__field:read-only:not(:disabled)) {
border-color: fox.color(input-border);
background: fox.color(input-surface);
}