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
// FoxModal — 시안: 통합관리자페이지 디자인시스템(KE9UILWhB8qTBe14uXUn4F)
// modal (190:1136 · 섹션 391:4640)
//
// 마크업 계약 (React 밖 소비자용):
// <dialog class="fox-modal fox-modal--md">
// <div class="fox-modal__body">
// <div class="fox-modal__tit"><h2 class="fox-modal__title">타이틀</h2></div> <!-- 선택 -->
// <div class="fox-modal__contents">…</div>
// </div>
// <div class="fox-modal__foot"> <!-- 선택 -->
// <div class="fox-modal__utility">…</div> <!-- 선택 -->
// <div class="fox-modal__actions">…</div> <!-- 선택 -->
// </div>
// <button class="fox-modal__close" type="button" aria-label="닫기">…svg…</button>
// </dialog>
// `<dialog>`는 `showModal()`로 열어야 뒷배경과 초점 가두기가 생긴다.
//
// 크기 넷(sm·md·lg·full)이 모두 폭 토큰이다. 모바일에서는 넷 다 같은 폭(348px)으로
// 모이고 여백·모서리·닫기 버튼도 함께 줄어든다 — 반응형 토큰이 그렇게 정의돼 있다.
//
// 제목 줄에만 오른쪽 여백을 준다 — 닫기 버튼이 그 자리에 겹쳐 앉기 때문이다. 본문과
// 아래쪽은 그 여백이 필요 없다.
//
// 닫기 버튼은 64px 칸의 아래쪽에 아이콘을 붙인다(시안 items-end). 그래야 아이콘이 제목
// 글줄과 눈높이가 맞는다. 상자 테두리 위에 얹히도록 1px 밖으로 내민다.
//
// 토큰이 아닌 값:
// - 아래쪽 버튼의 최소 폭 8rem: 시안 값이나 Figma가 변수로 export하지 않는다(사용자 승인).
// - 그림자 색: 시안이 테마 토큰이 아니라 `alpha/black10` 원시값에 직접 물려 놓았다
// (fox-carousel-button과 같은 사정).
// - 뒷배경을 덮는 방식(`::backdrop`)과 가운데 정렬: 구조 규칙.
// - line-height / letter-spacing: 시안 텍스트 스타일의 값이나 Figma가 변수로
// export하지 않는다.
@use "abstracts" as fox;
.fox-modal {
box-sizing: border-box;
// `<dialog>`는 닫혀 있을 때 브라우저가 숨기는데, `display`를 주면 그 기본값을 덮어
// 닫힌 모달이 화면에 그대로 남는다. 열렸을 때만 배치를 준다.
display: none;
flex-direction: column;
align-items: flex-start;
// `<dialog>`의 브라우저 기본 여백·테두리를 지우고 가운데에 놓는다.
max-inline-size: calc(100% - #{fox.padding(7)} * 2);
max-block-size: calc(100% - #{fox.padding(7)} * 2);
margin: auto;
padding: fox.modal(padding-default);
// 세로만 스크롤한다. `overflow: auto`로 두면 닫기 버튼이 테두리를 덮으려고 일부러 1px
// 밖으로 나간 것(`__close`의 `inset-inline-end: -1px`)까지 넘침으로 세어 가로 스크롤바가
// 생긴다 — 그 버튼은 배경도 테두리도 없고 아이콘이 8px 안쪽이라 x축을 잘라도 보이는 변화가
// 없다.
overflow-x: hidden;
overflow-y: auto;
border: fox.border(1) solid fox.color(border-neutral-subtle);
border-radius: fox.card(radius-xlg);
background: fox.color(surface-neutral-default);
color: fox.color(font-neutral-default);
// 시안 neutral/lv3 — 그림자 둘이 겹친다.
box-shadow:
fox.shadow(location4) fox.shadow(location4) fox.shadow(blur4) 0
fox.primitive(alpha-black10),
fox.shadow(location2) fox.shadow(location2) fox.shadow(blur3) 0
fox.primitive(alpha-black10);
&::backdrop {
background: fox.color(background-dim);
}
// 닫기 버튼이 상자 모서리에 얹히므로 기준점이 필요하다.
position: relative;
&[open] {
display: flex;
}
// ── 크기 ──────────────────────────────────────────────────────────────────
&--sm {
inline-size: fox.modal(width-sm);
}
&--md {
inline-size: fox.modal(width-md);
}
&--lg {
inline-size: fox.modal(width-lg);
}
&--full {
inline-size: fox.modal(width-full);
}
// ── 본문 ──────────────────────────────────────────────────────────────────
&__body {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: fox.gap(5);
inline-size: 100%;
}
// 닫기 버튼이 오른쪽 위에 앉으므로 제목만 그만큼 비켜 준다.
//
// 제목이 없어도 한 줄 높이를 남긴다 — 시안의 tit 프레임이 제목 없이도 28px(제목 글자
// 크기 × 줄높이)을 차지한다. 이 자리를 없애면 닫기 버튼이 내용 위로 내려앉는다.
&__tit {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: fox.gap(3);
inline-size: 100%;
min-block-size: calc(#{fox.font-size(heading-md)} * 1.4);
padding-inline-end: fox.padding(8);
}
&__title {
inline-size: 100%;
margin: 0;
color: fox.color(font-neutral-strong);
font-family: fox.font-family(title);
font-size: fox.font-size(heading-md);
font-weight: fox.font-weight(bold);
line-height: 1.4;
letter-spacing: -0.025em;
word-break: break-word;
}
&__contents {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: fox.gap(5);
inline-size: 100%;
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;
// 그림이 들어오면 폭을 채운다 — 시안의 contents 슬롯에 든 것도 폭 100%다.
> img,
> svg,
> picture {
inline-size: 100%;
block-size: auto;
}
}
// ── 아래쪽 ────────────────────────────────────────────────────────────────
&__foot {
display: flex;
align-items: center;
justify-content: flex-end;
gap: fox.gap(3);
inline-size: 100%;
padding-block-start: fox.padding(8);
}
// 왼편은 남는 폭을 먹어 버튼을 오른쪽 끝으로 민다.
&__utility {
display: flex;
flex: 1 0 0;
align-items: flex-start;
gap: fox.gap(3);
min-inline-size: 0;
}
&__actions {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: flex-end;
gap: fox.gap(3);
margin-inline-start: auto;
}
// 시안에서 모달 버튼은 최소 8rem이다 — 글자가 짧아도 두 버튼의 폭이 같아 보이게 한다.
// 버튼 자체의 성질이 아니라 이 자리의 규칙이라 여기서 준다(btn 컴포넌트 세트에는
// 최소 폭 표기가 없다).
&__actions .fox-button {
min-inline-size: 8rem;
}
// ── 닫기 ──────────────────────────────────────────────────────────────────
// 시안대로 아이콘을 칸 아래쪽에 붙여 제목 글줄과 눈높이를 맞춘다.
&__close {
position: absolute;
inset-block-start: calc(-1 * #{fox.border(1)});
inset-inline-end: calc(-1 * #{fox.border(1)});
display: flex;
align-items: flex-end;
inline-size: fox.modal(btn-close);
block-size: fox.modal(btn-close);
padding: fox.padding(3);
border: none;
background: none;
color: fox.color(icon-neutral-default);
cursor: pointer;
}
&__close-icon {
display: inline-flex;
> * {
inline-size: fox.icon(5);
block-size: fox.icon(5);
}
}
}