@fox — 포터블 디자인 시스템#
프로젝트에 종속되지 않는 디자인 시스템 패키지. 이 폴더를 통째로 복사하고 설정 2줄을
추가하면 다른 프로젝트에서 그대로 동작합니다.
@fox/
styles/ SCSS 토큰 · 함수 · 믹스인
index.scss 글로벌 진입점 (앱이 딱 한 번 import — CSS 출력)
abstracts.scss 저작 진입점 (컴포넌트가 @use — CSS 출력 0)
tokens/ 토큰 map = 단일 진실 공급원(SSOT)
_root.scss map → --fox-* 커스텀 프로퍼티 생성
_reset.scss 리셋 & 베이스
_functions.scss fox.color() 등 토큰 접근 + 검증
_mixins.scss fox.typo() · fox.media() 등 조합 패턴
core/
components/ 공용 컴포넌트 (규약은 components/README.md)
utils/ cx() 등 의존성 없는 유틸
다른 프로젝트로 이식하기#
@fox/폴더를 대상 프로젝트 루트에 복사합니다.sass를 설치합니다.npm install --save-dev sassTypeScript 경로 별칭 —
tsconfig.json:{ "compilerOptions": { "paths": { "@fox/*": ["./@fox/*"] } } }SCSS 로드 경로 —
next.config.ts:sassOptions: { loadPaths: [path.join(process.cwd())] }TypeScript의
paths는 번들러 전용이라 Sass의@use해석에는 관여하지 않습니다.
그래서 같은 경로를 양쪽에 각각 선언해야 TS와 SCSS의 import 표기가 일치합니다.
이게 없으면 컴포넌트마다../../../상대 경로를 써야 해서 폴더 복사가 불가능해집니다.앱의 글로벌 스타일에서 진입점을 한 번 import 합니다.
/* app/globals.scss */ @use "@fox/styles";(선택) 폰트를 주입합니다 — 아래 "호스트 앱과의 계약" 참고.
Next.js가 아닌 번들러라면 4번만 그 도구의 Sass 옵션(includePaths / loadPaths)으로
바꿔주면 됩니다.
핵심 설계#
토큰의 SSOT는 tokens/의 SCSS map 하나뿐입니다. 여기서 두 가지가 함께 파생됩니다 —_root.scss가 만드는 --fox-* CSS 커스텀 프로퍼티와, _functions.scss가 검증에 쓰는
유효 이름 목록. 값과 이름이 한 곳에만 있으므로 둘이 어긋날 수 없습니다.
규율을 컴파일러가 강제합니다. 화면 코드는 color: #4f46e5를 쓸 수 없고fox.color(primary)로 토큰을 지목해야 합니다. 없는 이름을 쓰면 빌드가 실패하며, 에러
메시지가 사용 가능한 토큰 목록을 함께 출력합니다.
Error: [@fox] 알 수 없는 color 토큰: `primry` — 사용 가능한 값: primary, primary-hover, ...
1rem = 10px입니다. _reset.scss의 html { font-size: 62.5% }가 근거입니다. px → rem
환산이 10으로 나누기가 되어 토큰 값이 읽기 쉽고, px 고정과 달리 사용자가 브라우저 글자
크기를 키우면 앱 전체가 비례해 확대됩니다.
⚠️ 미디어 쿼리 안의
rem은 예외입니다. 미디어 쿼리는html의font-size영향을
받지 않고 항상 브라우저 기본 크기(보통 16px) 기준으로 계산합니다. 그래서tokens/_layout.scss의 브레이크포인트48rem은 480px가 아니라 768px입니다.
각 값에 px 환산을 주석으로 달아 두었습니다.
라이트/다크는 값이 한 곳에만 존재합니다. light-dark(라이트, 다크)로 한 줄에 두 값을
함께 쓰고, 전환은 color-scheme이 담당합니다. 다크 전용 오버라이드 블록이 없으므로 한쪽만
고쳐서 생기는 테마 불일치가 구조적으로 불가능합니다.<html data-theme="light"|"dark">로 수동 선택을 덮어쓸 수 있고, 속성이 없으면 OS 설정을
따릅니다.
브라우저 지원 —
light-dark()자체는 Chrome 123 / Safari 17.5 / Firefox 120 이상이
필요하지만, Next.js 16(Turbopack)의 Lightning CSS가 현재 설정에서 이를var(--lightningcss-light, 라이트) var(--lightningcss-dark, 다크)조합으로 폴리필해
출력하므로 구형 브라우저에서도 동작합니다. browserslist 타깃을 좁히면 폴리필이 빠지고
원본light-dark()가 그대로 나가므로, 타깃을 바꿀 때 이 전제를 다시 확인하세요.
Lightning CSS를 쓰지 않는 번들러로 옮길 때도 마찬가지입니다.
호스트 앱과의 계약#
@fox가 소유하지 않고 앱에서 받는 값입니다.
| CSS 변수 | 용도 | 미주입 시 |
|---|---|---|
--app-font-sans | 기본 서체 | system-ui로 폴백 |
--app-font-mono | 고정폭 서체 | ui-monospace로 폴백 |
폰트 파일은 앱이 소유합니다(Next.js에서는 next/font가 최적화·자체 호스팅을 담당). @fox는
변수를 참조만 하므로, 폴더를 복사해 간 프로젝트는 그 프로젝트의 폰트를 그대로 씁니다.
// app/layout.tsx
const sans = localFont({ src: [...], variable: "--app-font-sans" });
<html className={sans.variable}>
테마 토글을 붙이려면 <html>의 data-theme 속성을 "light" | "dark"로 설정하고, 전환
애니메이션이 필요하면 같은 tick에 data-theme-transition 속성을 부여한 뒤 200ms 후
제거합니다(_root.scss가 그 동안만 색상 트랜지션을 겁니다).
토큰 추가하기#
tokens/의 해당 map에 항목을 추가합니다. 그것으로 끝입니다 — 커스텀 프로퍼티 생성과
함수 검증 목록이 자동으로 따라옵니다.- 값의 출처(Figma file/node 등)를 주석으로 남깁니다.
- 새 토큰 그룹(map 자체를 신설)이 필요하면
tokens/_index.scss에@forward를 추가하고,_functions.scss에 접근 함수를,_root.scss에@each블록을 각각 추가합니다.