@fox — 포터블 디자인 시스템#
프로젝트에 종속되지 않는 디자인 시스템. 이 폴더를 통째로 복사하고 설정 몇 줄을 추가하면
다른 프로젝트에서 그대로 동작합니다.
@fox/
styles/ SCSS — 여기가 디자인의 전부입니다
index.scss 토큰 진입점 (앱이 한 번 @use)
components.scss 공용 컴포넌트 스타일 묶음 진입점
_fox-button.scss 컴포넌트별 스타일 (개별 @use 가능)
abstracts.scss 저작 진입점 — 함수·믹스인 (CSS 출력 0)
tokens/ 토큰 map = 단일 진실 공급원 (자동 생성)
_root.scss map → --fox-* 커스텀 프로퍼티
_functions.scss fox.color() 등 토큰 접근 + 이름 검증
_mixins.scss fox.pc / fox.mobile
core/
components/ React 래퍼 (선택 — 아래 "React 없이 쓰기" 참고)
utils/ cx() 등 의존성 없는 유틸
dev-test/ 개발 전용 테스트·감사 화면
tools/build-tokens.py Figma JSON → SCSS 변환기
이식하기#
@fox/폴더를 대상 프로젝트에 복사합니다.sass를 설치합니다.npm install --save-dev sassSCSS 로드 경로를 설정합니다. Next.js면
next.config.ts:sassOptions: { loadPaths: [path.join(process.cwd())] }다른 번들러면 그 도구의 Sass 옵션(
includePaths/loadPaths)에 프로젝트 루트를
넣으면 됩니다. 이게 있어야@use "@fox/styles/..."같은 절대 표기가 해석되고, 없으면
컴포넌트마다../../../상대 경로를 써야 해서 폴더 복사가 불가능해집니다.글로벌 스타일에서 진입점을 불러옵니다.
@use "@fox/styles"; // 토큰 @use "@fox/styles/components"; // 공용 컴포넌트 스타일 (전부)골라 쓰려면 묶음 대신 개별 파티셜을 가져옵니다 — 안 쓰는 CSS가 번들에 실리지 않습니다.
둘을 같이 써도 Sass가 모듈을 한 번만 로드해 중복되지 않습니다.@use "@fox/styles/fox-button";(TypeScript 프로젝트에서 React 래퍼도 쓸 경우) 경로 별칭을 추가합니다.
{ "compilerOptions": { "paths": { "@fox/*": ["./@fox/*"] } } }(선택) 폰트를 주입합니다 — 아래 "호스트 앱과의 계약" 참고.
React 없이 쓰기#
디자인 레이어는 프레임워크에 의존하지 않습니다. 토큰도 컴포넌트 스타일도 순수 SCSS이고,
클래스명이 fox- 접두사 + BEM이라 전역에 풀어도 충돌하지 않습니다. Vue·Svelte·서버
템플릿·순수 HTML 어디서든 위 2~4번만 하고 마크업 계약을 지키면 같은 결과가 나옵니다.
각 컴포넌트의 마크업 계약은 해당 스타일 파일 상단 주석에 있습니다. 버튼은 이렇습니다.
<button class="fox-button fox-button--primary fox-button--md">
<span class="fox-button__icon"><!-- svg (선택) --></span>
<span class="fox-button__label">버튼</span>
<span class="fox-button__icon"><!-- svg (선택) --></span>
</button>
- 비활성은 네이티브
disabled속성으로 표현합니다(별도 클래스 없음). - 로딩은 앞 아이콘 자리에
fox-button__spinner를 두고disabled를 함께 겁니다. - 아이콘 SVG는
currentColor로 그려야 계열별 색이 적용됩니다. 크기는 슬롯이 정합니다.
core/의 React 컴포넌트는 이 클래스를 조립해주는 얇은 래퍼일 뿐입니다 — 쓰지 않아도
디자인은 그대로 얻습니다.
토큰만 쓰는 것도 물론 됩니다.
@use "@fox/styles/abstracts" as fox;
.whatever {
padding: fox.padding(6);
color: fox.color(font-neutral-default);
@include fox.pc { padding: fox.padding(8); }
}
핵심 설계#
토큰의 SSOT는 tokens/의 SCSS map 하나입니다. 여기서 두 가지가 함께 파생됩니다 —_root.scss가 만드는 --fox-* 커스텀 프로퍼티와, _functions.scss가 검증에 쓰는 유효
이름 목록. 값과 이름이 한 곳에만 있으므로 둘이 어긋날 수 없습니다.
토큰은 Figma에서 자동 생성됩니다. 손으로 고치지 않습니다. Figma 변수를 재수출한 뒤
변환기를 다시 돌리면 됩니다. 생성물이 재현 가능해서 재실행 diff가 곧 Figma 변경분입니다.
FOX_TOKENS_SRC=<export 폴더> python3 @fox/tools/build-tokens.py
규율을 컴파일러가 강제합니다. 화면 코드는 color: #256EF4를 쓸 수 없고fox.color(...)로 토큰을 지목해야 합니다. 없는 이름은 빌드가 실패하며 사용 가능한 목록을
함께 출력합니다.
Error: [@fox] 알 수 없는 color 토큰: `primry` — 사용 가능한 값: background-default, ...
1rem = 10px입니다. _root.scss의 html { font-size: 62.5% }가 근거입니다.
⚠️ 미디어 쿼리 안의
rem은 예외입니다. 미디어 쿼리는html의font-size영향을
받지 않습니다. 그래서 브레이크포인트는768px로 두었고, 미디어 쿼리는fox.pc/fox.mobile믹스인으로만 씁니다 — 조건부는var()를 해석하지 못합니다.
라이트/다크는 값이 한 곳에만 존재합니다. light-dark(라이트, 다크)로 한 줄에 두 값을
쓰고 전환은 color-scheme이 담당합니다. 다크 전용 오버라이드 블록이 없어 테마 불일치가
구조적으로 불가능합니다. <html data-theme="light"|"dark">로 수동 선택을 덮어쓸 수 있고,
속성이 없으면 OS 설정을 따릅니다.
light-dark()자체는 Chrome 123 / Safari 17.5 / Firefox 120 이상이 필요하지만,
Lightning CSS(Next.js Turbopack)가 커스텀 프로퍼티 조합으로 폴리필해 출력하므로 구형
브라우저에서도 동작합니다. browserslist 타깃을 좁히거나 다른 번들러로 옮길 때 재확인하세요.
호스트 앱과의 계약#
@fox가 소유하지 않고 앱에서 받는 값입니다.
| CSS 변수 | 용도 | 미주입 시 |
|---|---|---|
--app-font-sans | 기본 서체 | system-ui로 폴백 |
--app-font-mono | 고정폭 서체 | ui-monospace로 폴백 |
폰트 파일은 앱이 소유합니다. @fox는 변수를 참조만 하므로, 복사해 간 프로젝트는 그
프로젝트의 폰트를 그대로 씁니다.
새 컴포넌트 추가#
core/components/README.md의 작성 규약을 따릅니다. 요점은 스타일을 styles/_<name>.scss에
두고 fox- 접두사 + BEM으로 이름 짓는 것 — 그래야 React 밖에서도 쓸 수 있습니다.