// 개발 전용 테스트 페이지 스타일. // // 이 파일 자체가 토큰 규율의 첫 소비처다 — 여기 있는 모든 값은 `fox.*()` 함수를 거치므로, // 없는 토큰을 쓰면 빌드가 실패한다. 페이지가 그려진다는 것은 사용된 토큰이 전부 실재한다는 // 뜻이기도 하다. @use "@fox/styles/abstracts" as fox; .shell { display: grid; grid-template-columns: 1fr; min-block-size: 100dvh; background: fox.color(background-default); color: fox.color(font-neutral-default); @include fox.pc { // 화면을 넘지 않게 못박고 스크롤은 두 칸이 각자 갖는다. block-size: 100dvh; grid-template-columns: 24rem 1fr; overflow: hidden; } } .loading { padding: fox.padding(8); color: fox.color(font-neutral-subtle); font-size: fox.font-size(body-md); } // ── 사이드바 ──────────────────────────────────────────────────────────────── // 본 프로젝트 사이드바(app/(protected)/(basic)/_components/admin-sidebar)와 같은 생김새다. // @fox 폴더만 복사해도 하니스가 따라가야 해서 규칙을 옮겨 적었다. .sidebar { box-sizing: border-box; display: flex; flex-direction: column; padding: fox.padding(5); border-block-end: fox.border(1) solid fox.color(border-neutral-subtler); background: linear-gradient( to bottom, fox.color(surface-neutral-gray) 70%, fox.color(surface-theme-primary) ); @include fox.pc { min-block-size: 0; overflow-y: auto; border-block-end: 0; box-shadow: inset calc(-1 * #{fox.border(1)}) 0 0 0 fox.color(border-neutral-subtler); } } .top { display: flex; flex-direction: column; gap: fox.gap(3); padding-block-end: fox.spacing(bottom-sm); border-block-end: fox.border(1) solid fox.color(border-neutral-subtle); } // FoxInput의 기본 테두리는 input-border(진한 회색)인데 시안 검색창은 border-neutral-subtler다. .search :global(.fox-input__box) { border-color: fox.color(border-neutral-subtler); } // 세그먼티드 컨트롤은 inline-flex라 내용만큼만 넓다 — 늘리지 않고 줄 가운데에 세운다. .themeRow { display: flex; justify-content: center; } .empty { padding-block: fox.spacing(top-md); color: fox.color(font-neutral-subtler); font-size: fox.font-size(label-sm); } // ── 섹션 ──────────────────────────────────────────────────────────────────── .section { display: flex; inline-size: 100%; flex-direction: column; align-items: flex-start; padding-block-start: fox.spacing(top-sm); padding-block-end: fox.spacing(bottom-md); } .sectionTitle { inline-size: 100%; margin: 0; padding-block-end: fox.spacing(bottom-sm); color: fox.color(font-neutral-subtler); font-family: fox.font-family(title); font-size: fox.font-size(heading-2xsm); font-weight: fox.font-weight(bold); line-height: 1.4; letter-spacing: -0.025em; } .list { display: flex; inline-size: 100%; flex-direction: column; align-items: flex-start; margin: 0; padding: 0; list-style: none; } .item { inline-size: 100%; } // ── 1depth ────────────────────────────────────────────────────────────────── .itemButton { display: flex; inline-size: 100%; align-items: center; gap: fox.gap(2); padding-block: fox.padding(4); padding-inline: fox.padding(3); border: none; border-radius: fox.radius(3); background: fox.color(action-secondary); color: fox.color(font-neutral-subtle); font-family: fox.font-family(title); font-size: 1.4rem; font-weight: fox.font-weight(bold); line-height: 1.4; letter-spacing: -0.025em; text-align: start; cursor: pointer; mix-blend-mode: multiply; } .itemButtonActive { background: fox.color(action-secondary-active); color: fox.color(font-neutral-strong-inverse); mix-blend-mode: normal; } .itemLabel { flex: 1 0 0; min-inline-size: 0; } // ── 2depth ────────────────────────────────────────────────────────────────── .childGroup { display: flex; inline-size: 100%; flex-direction: column; align-items: flex-start; margin: fox.gap(2) 0 0; padding: fox.padding(3); border-radius: fox.radius(3); box-shadow: inset 0 0 0 fox.border(1) fox.color(border-neutral-subtler); background: fox.color(surface-neutral-default); list-style: none; } .child { inline-size: 100%; } .childRow { display: flex; inline-size: 100%; align-items: center; gap: fox.gap(2); padding-block: fox.padding(2); padding-inline: fox.padding(3); border: none; border-radius: fox.radius(3); background: fox.color(action-secondary); color: fox.color(font-neutral-default); font-family: fox.font-family(title); font-size: 1.4rem; font-weight: fox.font-weight(bold); line-height: 1.4; letter-spacing: -0.025em; text-align: start; cursor: pointer; } .childRowActive { color: fox.color(font-theme-secondary); } // 별·캐럿 자리는 항상 잡아 두고 필요 없을 때 투명하게만 만든다 — 라벨 시작 위치가 // 항목마다 흔들리지 않게 하기 위해서다. .childMark { display: inline-flex; flex-shrink: 0; inline-size: fox.icon(2); font-size: fox.icon(2); block-size: fox.icon(2); } .childMark > svg { inline-size: 100%; block-size: 100%; } .childMarkHidden { opacity: 0; } .childLabel { flex: 1 0 0; min-inline-size: 0; } // ── 3depth ────────────────────────────────────────────────────────────────── .leafGroup { display: flex; inline-size: 100%; flex-direction: column; align-items: flex-start; margin: 0; padding-block: fox.padding(2); padding-inline-start: fox.padding(7); padding-inline-end: fox.padding(2); border-radius: fox.radius(3); list-style: none; } .leaf { display: flex; inline-size: 100%; align-items: center; gap: fox.gap(2); padding-block: fox.padding(1); padding-inline: fox.padding(2); border: none; background: none; color: fox.color(font-neutral-subtle); font-family: fox.font-family(body); font-size: 1.3rem; line-height: 1.5; letter-spacing: -0.025em; text-align: start; cursor: pointer; } .leafActive { color: fox.color(font-theme-secondary); } // 시안에 없는 상태 — 키보드로 훑을 수 있어야 해서 남긴다. .itemButton:focus-visible, .childRow:focus-visible, .leaf:focus-visible { outline: fox.border(2) solid fox.color(border-theme-primary); outline-offset: fox.border(1); } // 시안에 없는 컴포넌트별 개수 표시. 캐럿이 서는 자리에 둔다. .count { flex-shrink: 0; } // "추가 방법"은 컴포넌트가 아니라 안내다. 목록 맨 아래에 두고 테두리로 구분해, 등록된 // 컴포넌트와 같은 층위로 읽히지 않게 한다. .guideButton { margin-block-start: fox.gap(2); box-shadow: inset 0 0 0 fox.border(1) fox.color(border-neutral-strong); background: none; color: fox.color(font-neutral-subtler); mix-blend-mode: normal; } .guideButtonActive { box-shadow: inset 0 0 0 fox.border(1) fox.color(border-theme-primary); color: fox.color(font-theme-primary); } .guideMark { font-size: fox.font-size(label-lg); line-height: 1; } // ── 본문 ──────────────────────────────────────────────────────────────────── .content { min-inline-size: 0; padding: fox.spacing(top-md) fox.padding(7) fox.spacing(bottom-xlg); @include fox.pc { min-block-size: 0; overflow-y: auto; } @include fox.pc { padding: fox.spacing(top-lg) fox.padding(11) fox.spacing(bottom-xlg); } } .section { display: flex; flex-direction: column; } .sectionTitle { font-size: fox.font-size(heading-md); font-weight: fox.font-weight(bold); } .sectionNote { margin-block: fox.gap(2) fox.gap(6); color: fox.color(font-neutral-subtle); font-size: fox.font-size(body-sm); } .code { overflow-x: auto; margin-block-start: fox.gap(4); padding: fox.padding(6); border: fox.border(1) solid fox.color(border-neutral-subtle); border-radius: fox.radius(3); background: fox.color(surface-neutral-gray); color: fox.color(font-neutral-default); font-size: fox.font-size(body-sm); } // ── 색상 칩 ───────────────────────────────────────────────────────────────── // 값 앞에 붙는 16px 정사각형. 16px는 `icon-3`(1.6rem)과 같은 값이라 리터럴 대신 // 토큰을 쓴다. // 테두리는 box-shadow로 그린다 — border는 칩 안쪽을 먹어 색이 보이는 면적을 줄인다. .colorChip { display: inline-block; inline-size: fox.icon(3); block-size: fox.icon(3); margin-inline-end: fox.gap(3); box-shadow: 0 0 0 fox.border(1) #{fox.color(border-neutral-overlay)}; vertical-align: middle; } .tokenName { display: block; color: fox.color(font-neutral-default); font-size: fox.font-size(label-sm); font-weight: fox.font-weight(medium); word-break: break-all; } // 다른 토큰을 가리키는 값. 실제 값과 구분되어야 "이게 참조구나"가 한눈에 보인다. .aliasRef { color: fox.color(font-theme-primary); } // 서체 토큰 미리보기 문장. .demoType { margin: 0; color: fox.color(font-neutral-default); white-space: nowrap; } // ── 값 테이블 ─────────────────────────────────────────────────────────────── .table { inline-size: 100%; border-collapse: collapse; text-align: start; } .table th, .table td { padding: fox.padding(3) fox.padding(5); border-block-end: fox.border(1) solid fox.color(border-neutral-subtle); } .table th { color: fox.color(font-neutral-subtler); font-size: fox.font-size(label-xsm); font-weight: fox.font-weight(medium); } .table td { color: fox.color(font-neutral-default); font-size: fox.font-size(body-sm); } .tdValue { color: fox.color(font-neutral-subtle); } // 미리보기 칸은 내용만큼만 차지하게 두어 토큰/값 열이 밀리지 않게 한다. .tdDemo { inline-size: 1%; white-space: nowrap; } // ── 패딩 · 간격 미리보기 ──────────────────────────────────────────────────── // 점선 테두리와 옅은 바탕이 "토큰이 만든 공간"이고, 흰 칩이 문단이다. .demoPadBox, .demoGapBox { border: fox.border(1) dashed fox.color(border-theme-primary); background: fox.color(surface-theme-primary); } .demoPadBox { display: inline-block; } .demoGapBox { display: inline-flex; align-items: center; } .demoText { margin: 0; padding: fox.padding(1) fox.padding(3); background: fox.color(surface-neutral-default); color: fox.color(font-neutral-default); font-size: fox.font-size(label-xsm); white-space: nowrap; } // ── 모서리 반경 · 테두리 두께 미리보기 ────────────────────────────────────── .demoRadius, .demoBorder { inline-size: fox.icon(7); block-size: fox.icon(7); border-color: fox.color(border-theme-primary); border-style: solid; background: fox.color(surface-neutral-gray); } .demoRadius { border-width: fox.border(2); } // ── 아이콘 크기 미리보기 ──────────────────────────────────────────────────── // 브라우저 기본 체크박스를 토큰 크기로 키운다 — 별도 에셋 없이 크기만 비교한다. .demoIcon { display: block; margin: 0; accent-color: fox.color(surface-theme-primary); } // ── 그림자 미리보기 ───────────────────────────────────────────────────────── // 그림자 색은 Figma에 토큰이 없어 중립 알파 토큰을 데모용으로 쓴다. // 인라인 style이 참조할 수 있도록 여기서 커스텀 프로퍼티로 내려둔다. .demoShadow { --demo-shadow-color: #{fox.color(alpha-default-inverse25)}; inline-size: fox.icon(7); block-size: fox.icon(6); border-radius: fox.radius(2); background: fox.color(surface-neutral-default); } // ── 배경 흐림 미리보기 ────────────────────────────────────────────────────── // 캔버스 크기는 디자인 값이 아니라 데모용 지면이라 토큰을 두지 않는다. .demoBackdrop { position: relative; overflow: hidden; inline-size: 20rem; block-size: 12rem; border-radius: fox.radius(2); } .demoBackdropImage { display: block; inline-size: 100%; block-size: 100%; object-fit: cover; } .demoBackdropOverlay { position: absolute; inset-block: 0; inset-inline-end: 0; inline-size: 50%; border-inline-start: fox.border(1) solid fox.color(border-neutral-overlay); } // ── 컴포넌트 예제 ─────────────────────────────────────────────────────────── .variantList { display: flex; flex-direction: column; gap: fox.gap(5); } .variant { display: flex; flex-direction: column; gap: fox.gap(2); } .variantLabel { color: fox.color(font-neutral-subtler); font-size: fox.font-size(label-xsm); font-weight: fox.font-weight(medium); } // 예제를 얹는 무대. 컴포넌트 자체의 배경·여백과 섞이지 않도록 중립 표면 위에 올린다. .variantStage { display: flex; flex-wrap: wrap; gap: fox.gap(4); align-items: center; padding: fox.padding(7); border: fox.border(1) solid fox.color(border-neutral-subtle); border-radius: fox.radius(3); background: fox.color(surface-neutral-default); } // 그룹 예제용. 그룹이 블록 레벨이라 정렬·줄바꿈을 보려면 폭이 정해진 무대가 필요하다. // // ⚠️ 세로 flex의 기본 `align-items: stretch`가 자식을 가로로 늘린다. 인라인 컨트롤 // (`inline-flex`·`inline-block`인 것들)을 여러 줄로 쌓아 비교할 때는 아래 `demoStackStart`를 // 쓴다 — 이걸 쓰면 컴포넌트가 제 폭을 잃어 실제 모습과 다르게 보인다. .demoStack { display: flex; flex-direction: column; gap: fox.gap(4); inline-size: 100%; } // 인라인 컨트롤을 세로로 쌓아 비교할 때. 자식이 늘어나지 않아 제 폭 그대로 보인다. .demoStackStart { display: flex; flex-direction: column; align-items: flex-start; gap: fox.gap(4); } .demoOutline { padding: fox.padding(3); border: fox.border(1) dashed fox.color(border-neutral-subtle); border-radius: fox.radius(2); inline-size: 100%; } // 폼 컨트롤 예제용. FoxSelect는 폭을 부모가 정하므로 무대가 폭을 줘야 크기 비교가 된다. // 칸 너비는 디자인 값이 아니라 데모용 지면이라 토큰을 두지 않는다. .demoFields { display: flex; flex-wrap: wrap; gap: fox.gap(5); align-items: start; inline-size: 100%; > * { inline-size: 28rem; max-inline-size: 100%; } } // 옵션 목록 단독 예제용. 목록이 `position: absolute`라 위치 기준이 필요하고, 자기 높이를 // 차지하지 않으므로 아래쪽 여백으로 펼쳐질 자리를 만들어 준다. 높이 0이면 목록이 무대 // 맨 위에서 시작한다. 치수는 데모용 지면이라 토큰을 두지 않는다. .demoListboxStage { position: relative; inline-size: 28rem; max-inline-size: 100%; block-size: 0; margin-block-end: 22rem; } // wrap 예제는 자식이 확실히 넘치도록 좁아야 한다. .demoNarrow { padding: fox.padding(3); border: fox.border(1) dashed fox.color(border-neutral-subtle); border-radius: fox.radius(2); inline-size: 20rem; max-inline-size: 100%; } // 입력 예제용. 시안 폭(360px)에 맞춰야 실제 비율로 보인다. .demoField { inline-size: 36rem; max-inline-size: 100%; } // 이메일은 세 칸이라 360px에서는 계속 줄이 넘어간다. 넓게 볼 무대를 따로 둔다. .demoFieldWide { inline-size: 80rem; max-inline-size: 100%; } // 캐러셀 조각은 흰 알약이라 흰 바탕에서는 보이지 않는다 — 시안처럼 어두운 판 위에 올린다. .carouselStage { display: flex; flex-wrap: wrap; gap: fox.gap(5); align-items: center; padding: fox.padding(6); border-radius: fox.radius(3); background: fox.color(surface-neutral-gray-stronger); } // 조합을 한 줄에 늘어놓고 비교할 때. 좁아지면 줄이 넘어간다. .demoRow { display: flex; flex-wrap: wrap; align-items: center; gap: fox.gap(3); } // 예시 하나에 이름표를 붙여 세로로 세운다 — 아이콘처럼 무엇인지 글자로 알려야 하는 자리용. .demoLabelled { display: inline-flex; flex-direction: column; align-items: center; gap: fox.gap(2); color: fox.color(font-neutral-subtle); font-size: fox.font-size(body-xsm); } // 정렬 가능한 머리칸 — 글자 옆에 위아래 화살표가 붙는다. .tableSortHead { display: inline-flex; align-items: center; gap: fox.gap(2); white-space: nowrap; > svg { inline-size: fox.icon(3); block-size: fox.icon(3); color: fox.color(icon-neutral-default); } } // 스크롤바를 보이려면 넘치는 상자가 있어야 한다 — 그 상자를 만드는 데모 전용 규칙. .scrollDemo { overflow-y: auto; inline-size: 100%; max-inline-size: 32rem; block-size: 16rem; padding: fox.padding(6); border: fox.border(1) solid fox.color(border-neutral-subtle); border-radius: fox.radius(3); background: fox.color(surface-neutral-default); } .scrollDemoRow { overflow-x: auto; inline-size: 100%; max-inline-size: 32rem; padding: fox.padding(6); border: fox.border(1) solid fox.color(border-neutral-subtle); border-radius: fox.radius(3); background: fox.color(surface-neutral-default); > p { inline-size: 64rem; } }