ldu0009 08-11 13bf742 feat: @fox 포터블 디자인 시스템 패키지 추가 UNIX

@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() 등 의존성 없는 유틸

다른 프로젝트로 이식하기#

  1. @fox/ 폴더를 대상 프로젝트 루트에 복사합니다.

  2. sass를 설치합니다.

    npm install --save-dev sass
  3. TypeScript 경로 별칭 — tsconfig.json:

    { "compilerOptions": { "paths": { "@fox/*": ["./@fox/*"] } } }
  4. SCSS 로드 경로 — next.config.ts:

    sassOptions: { loadPaths: [path.join(process.cwd())] }

    TypeScript의 paths는 번들러 전용이라 Sass의 @use 해석에는 관여하지 않습니다.
    그래서 같은 경로를 양쪽에 각각 선언해야 TS와 SCSS의 import 표기가 일치합니다.
    이게 없으면 컴포넌트마다 ../../../ 상대 경로를 써야 해서 폴더 복사가 불가능해집니다.

  5. 앱의 글로벌 스타일에서 진입점을 한 번 import 합니다.

    /* app/globals.scss */
    @use "@fox/styles";
  6. (선택) 폰트를 주입합니다 — 아래 "호스트 앱과의 계약" 참고.

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가 그 동안만 색상 트랜지션을 겁니다).

토큰 추가하기#

  1. tokens/의 해당 map에 항목을 추가합니다. 그것으로 끝입니다 — 커스텀 프로퍼티 생성과
    함수 검증 목록이 자동으로 따라옵니다.
  2. 값의 출처(Figma file/node 등)를 주석으로 남깁니다.
  3. 새 토큰 그룹(map 자체를 신설)이 필요하면 tokens/_index.scss에 @forward를 추가하고,
    _functions.scss에 접근 함수를, _root.scss에 @each 블록을 각각 추가합니다.