// 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: 구조 규칙. @use "abstracts" as fox; .fox-file-upload { display: flex; flex-direction: column; gap: fox.gap(3); inline-size: 100%; // area 모드는 상자와 목록 사이가 한 단계 넓다. &--area { gap: fox.gap(7); } &__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; } &__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: cover; } &__thumb-remove { position: absolute; inset-block-start: 50%; inset-inline-start: 50%; transform: translate(-50%, -50%); } &__image-side { display: flex; flex: 1 0 0; flex-direction: column; gap: fox.gap(4); min-inline-size: 0; } // ── 목록 ────────────────────────────────────────────────────────────────── // 줄이 많아지면 상자 안에서만 스크롤한다. &__list { display: flex; flex-direction: column; inline-size: 100%; max-block-size: 32rem; overflow-x: clip; overflow-y: auto; border: fox.border(1) solid fox.color(border-neutral-subtle); border-radius: fox.radius(3); } &__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); }