revert: 근거 없이 생성된 디자인 토큰 값 제거
색상·타이포·간격·radius·그림자·모션·브레이크포인트 토큰은 실제 디자인 자료 없이 임의로 만들어진 값이었다. 디자인 시스템은 제공된 자료로만 구축한다는 원칙에 따라 값과 그에 딸린 함수·믹스인·리셋을 모두 제거한다. 자료를 받는 시점에 구조부터 다시 세운다. Co-Authored-By: Claude Opus 5
@c3d840ea9964adc4e828d8ad38a067364f0a97cf
--- @fox/README.md
... | ... | @@ -1,123 +0,0 @@ |
| 1 | -# @fox — 포터블 디자인 시스템 | |
| 2 | - | |
| 3 | -프로젝트에 종속되지 않는 디자인 시스템 패키지. **이 폴더를 통째로 복사하고 설정 2줄을 | |
| 4 | -추가하면** 다른 프로젝트에서 그대로 동작합니다. | |
| 5 | - | |
| 6 | -``` | |
| 7 | -@fox/ | |
| 8 | - styles/ SCSS 토큰 · 함수 · 믹스인 | |
| 9 | - index.scss 글로벌 진입점 (앱이 딱 한 번 import — CSS 출력) | |
| 10 | - abstracts.scss 저작 진입점 (컴포넌트가 @use — CSS 출력 0) | |
| 11 | - tokens/ 토큰 map = 단일 진실 공급원(SSOT) | |
| 12 | - _root.scss map → --fox-* 커스텀 프로퍼티 생성 | |
| 13 | - _reset.scss 리셋 & 베이스 | |
| 14 | - _functions.scss fox.color() 등 토큰 접근 + 검증 | |
| 15 | - _mixins.scss fox.typo() · fox.media() 등 조합 패턴 | |
| 16 | - core/ | |
| 17 | - components/ 공용 컴포넌트 (규약은 components/README.md) | |
| 18 | - utils/ cx() 등 의존성 없는 유틸 | |
| 19 | -``` | |
| 20 | - | |
| 21 | -## 다른 프로젝트로 이식하기 | |
| 22 | - | |
| 23 | -1. `@fox/` 폴더를 대상 프로젝트 루트에 복사합니다. | |
| 24 | - | |
| 25 | -2. `sass`를 설치합니다. | |
| 26 | - | |
| 27 | - ```bash | |
| 28 | - npm install --save-dev sass | |
| 29 | - ``` | |
| 30 | - | |
| 31 | -3. **TypeScript 경로 별칭** — `tsconfig.json`: | |
| 32 | - | |
| 33 | - ```json | |
| 34 | - { "compilerOptions": { "paths": { "@fox/*": ["./@fox/*"] } } } | |
| 35 | - ``` | |
| 36 | - | |
| 37 | -4. **SCSS 로드 경로** — `next.config.ts`: | |
| 38 | - | |
| 39 | - ```ts | |
| 40 | - sassOptions: { loadPaths: [path.join(process.cwd())] } | |
| 41 | - ``` | |
| 42 | - | |
| 43 | - TypeScript의 `paths`는 번들러 전용이라 Sass의 `@use` 해석에는 관여하지 않습니다. | |
| 44 | - 그래서 같은 경로를 양쪽에 각각 선언해야 TS와 SCSS의 import 표기가 일치합니다. | |
| 45 | - 이게 없으면 컴포넌트마다 `../../../` 상대 경로를 써야 해서 폴더 복사가 불가능해집니다. | |
| 46 | - | |
| 47 | -5. 앱의 글로벌 스타일에서 진입점을 한 번 import 합니다. | |
| 48 | - | |
| 49 | - ```scss | |
| 50 | - /* app/globals.scss */ | |
| 51 | - @use "@fox/styles"; | |
| 52 | - ``` | |
| 53 | - | |
| 54 | -6. (선택) 폰트를 주입합니다 — 아래 "호스트 앱과의 계약" 참고. | |
| 55 | - | |
| 56 | -Next.js가 아닌 번들러라면 4번만 그 도구의 Sass 옵션(`includePaths` / `loadPaths`)으로 | |
| 57 | -바꿔주면 됩니다. | |
| 58 | - | |
| 59 | -## 핵심 설계 | |
| 60 | - | |
| 61 | -**토큰의 SSOT는 `tokens/`의 SCSS map 하나뿐입니다.** 여기서 두 가지가 함께 파생됩니다 — | |
| 62 | -`_root.scss`가 만드는 `--fox-*` CSS 커스텀 프로퍼티와, `_functions.scss`가 검증에 쓰는 | |
| 63 | -유효 이름 목록. 값과 이름이 한 곳에만 있으므로 둘이 어긋날 수 없습니다. | |
| 64 | - | |
| 65 | -**규율을 컴파일러가 강제합니다.** 화면 코드는 `color: #4f46e5`를 쓸 수 없고 | |
| 66 | -`fox.color(primary)`로 토큰을 지목해야 합니다. 없는 이름을 쓰면 빌드가 실패하며, 에러 | |
| 67 | -메시지가 사용 가능한 토큰 목록을 함께 출력합니다. | |
| 68 | - | |
| 69 | -``` | |
| 70 | -Error: [@fox] 알 수 없는 color 토큰: `primry` — 사용 가능한 값: primary, primary-hover, ... | |
| 71 | -``` | |
| 72 | - | |
| 73 | -**1rem = 10px입니다.** `_reset.scss`의 `html { font-size: 62.5% }`가 근거입니다. px → rem | |
| 74 | -환산이 10으로 나누기가 되어 토큰 값이 읽기 쉽고, px 고정과 달리 사용자가 브라우저 글자 | |
| 75 | -크기를 키우면 앱 전체가 비례해 확대됩니다. | |
| 76 | - | |
| 77 | -> ⚠️ **미디어 쿼리 안의 `rem`은 예외입니다.** 미디어 쿼리는 `html`의 `font-size` 영향을 | |
| 78 | -> 받지 않고 항상 브라우저 기본 크기(보통 16px) 기준으로 계산합니다. 그래서 | |
| 79 | -> `tokens/_layout.scss`의 브레이크포인트 `48rem`은 480px가 아니라 **768px**입니다. | |
| 80 | -> 각 값에 px 환산을 주석으로 달아 두었습니다. | |
| 81 | - | |
| 82 | -**라이트/다크는 값이 한 곳에만 존재합니다.** `light-dark(라이트, 다크)`로 한 줄에 두 값을 | |
| 83 | -함께 쓰고, 전환은 `color-scheme`이 담당합니다. 다크 전용 오버라이드 블록이 없으므로 한쪽만 | |
| 84 | -고쳐서 생기는 테마 불일치가 구조적으로 불가능합니다. | |
| 85 | -`<html data-theme="light"|"dark">`로 수동 선택을 덮어쓸 수 있고, 속성이 없으면 OS 설정을 | |
| 86 | -따릅니다. | |
| 87 | - | |
| 88 | -> **브라우저 지원** — `light-dark()` 자체는 Chrome 123 / Safari 17.5 / Firefox 120 이상이 | |
| 89 | -> 필요하지만, Next.js 16(Turbopack)의 Lightning CSS가 현재 설정에서 이를 | |
| 90 | -> `var(--lightningcss-light, 라이트) var(--lightningcss-dark, 다크)` 조합으로 폴리필해 | |
| 91 | -> 출력하므로 구형 브라우저에서도 동작합니다. browserslist 타깃을 좁히면 폴리필이 빠지고 | |
| 92 | -> 원본 `light-dark()`가 그대로 나가므로, 타깃을 바꿀 때 이 전제를 다시 확인하세요. | |
| 93 | -> Lightning CSS를 쓰지 않는 번들러로 옮길 때도 마찬가지입니다. | |
| 94 | - | |
| 95 | -## 호스트 앱과의 계약 | |
| 96 | - | |
| 97 | -`@fox`가 소유하지 않고 앱에서 받는 값입니다. | |
| 98 | - | |
| 99 | -| CSS 변수 | 용도 | 미주입 시 | | |
| 100 | -| --- | --- | --- | | |
| 101 | -| `--app-font-sans` | 기본 서체 | `system-ui`로 폴백 | | |
| 102 | -| `--app-font-mono` | 고정폭 서체 | `ui-monospace`로 폴백 | | |
| 103 | - | |
| 104 | -폰트 파일은 앱이 소유합니다(Next.js에서는 `next/font`가 최적화·자체 호스팅을 담당). `@fox`는 | |
| 105 | -변수를 참조만 하므로, 폴더를 복사해 간 프로젝트는 그 프로젝트의 폰트를 그대로 씁니다. | |
| 106 | - | |
| 107 | -```tsx | |
| 108 | -// app/layout.tsx | |
| 109 | -const sans = localFont({ src: [...], variable: "--app-font-sans" }); | |
| 110 | -<html className={sans.variable}> | |
| 111 | -``` | |
| 112 | - | |
| 113 | -테마 토글을 붙이려면 `<html>`의 `data-theme` 속성을 `"light" | "dark"`로 설정하고, 전환 | |
| 114 | -애니메이션이 필요하면 같은 tick에 `data-theme-transition` 속성을 부여한 뒤 200ms 후 | |
| 115 | -제거합니다(`_root.scss`가 그 동안만 색상 트랜지션을 겁니다). | |
| 116 | - | |
| 117 | -## 토큰 추가하기 | |
| 118 | - | |
| 119 | -1. `tokens/`의 해당 map에 항목을 추가합니다. 그것으로 끝입니다 — 커스텀 프로퍼티 생성과 | |
| 120 | - 함수 검증 목록이 자동으로 따라옵니다. | |
| 121 | -2. 값의 출처(Figma file/node 등)를 주석으로 남깁니다. | |
| 122 | -3. **새 토큰 그룹**(map 자체를 신설)이 필요하면 `tokens/_index.scss`에 `@forward`를 추가하고, | |
| 123 | - `_functions.scss`에 접근 함수를, `_root.scss`에 `@each` 블록을 각각 추가합니다. |
--- @fox/core/components/README.md
... | ... | @@ -1,87 +0,0 @@ |
| 1 | -# 컴포넌트 작성 규약 | |
| 2 | - | |
| 3 | -아직 컴포넌트가 없습니다. 디자인 확정에 따라 하나씩 추가합니다. | |
| 4 | - | |
| 5 | -## 파일 배치 | |
| 6 | - | |
| 7 | -컴포넌트 하나당 폴더 하나. 스타일을 같은 폴더에 두어야 폴더째 복사·삭제가 가능합니다. | |
| 8 | - | |
| 9 | -``` | |
| 10 | -components/ | |
| 11 | - button/ | |
| 12 | - button.tsx 컴포넌트 | |
| 13 | - button.module.scss 스타일 (반드시 .module.scss — 전역 오염 방지) | |
| 14 | - index.ts export { Button } from "./button"; | |
| 15 | - index.ts 배럴 — export * from "./button"; | |
| 16 | -``` | |
| 17 | - | |
| 18 | -## 작성 규칙 | |
| 19 | - | |
| 20 | -**1. 스타일은 `abstracts`만 `@use` 한다.** | |
| 21 | - | |
| 22 | -```scss | |
| 23 | -@use "@fox/styles/abstracts" as fox; | |
| 24 | - | |
| 25 | -.button { | |
| 26 | - padding: fox.space(2) fox.space(4); | |
| 27 | - border-radius: fox.radius(md); | |
| 28 | - background: fox.color(primary); | |
| 29 | - color: fox.color(on-primary); | |
| 30 | - | |
| 31 | - @include fox.typo(label-lg); | |
| 32 | - @include fox.focus-ring; | |
| 33 | - @include fox.transition((background-color, color)); | |
| 34 | -} | |
| 35 | -``` | |
| 36 | - | |
| 37 | -`@fox/styles`(진입점)를 `@use` 하면 안 됩니다 — 그쪽은 CSS를 출력하므로 컴포넌트마다 | |
| 38 | -토큰 선언 전체가 복제됩니다. `abstracts`는 출력이 0입니다. | |
| 39 | - | |
| 40 | -**2. 원시 디자인 값을 쓰지 않는다.** | |
| 41 | - | |
| 42 | -`color: #4f46e5`, `padding: 13px`, `border-radius: 12px` 금지. 전부 `fox.*()` 함수로 | |
| 43 | -토큰을 지목합니다. 없는 토큰 이름을 쓰면 빌드가 실패하므로 오타는 배포까지 갈 수 | |
| 44 | -없습니다. **맞는 토큰이 없다고 해서 원시 값을 쓰지 말고, 토큰을 먼저 추가**합니다 | |
| 45 | -(`@fox/styles/tokens/`). | |
| 46 | - | |
| 47 | -예외는 의미가 고정되어 토큰화가 무의미한 값뿐입니다 — `0`, `100%`, `1px`(구분선), | |
| 48 | -`auto`, `inherit`. | |
| 49 | - | |
| 50 | -**3. 기본은 Server Component로 둔다.** | |
| 51 | - | |
| 52 | -`'use client'`는 상호작용(상태·이벤트 핸들러·브라우저 API)이 실제로 필요한 컴포넌트에만 | |
| 53 | -붙입니다. CSS Module은 빌드타임에 클래스명 문자열로 바뀔 뿐이라 런타임 JS가 없으므로, | |
| 54 | -스타일링만 하는 컴포넌트는 서버에 남을 수 있습니다. 여기에 불필요하게 `'use client'`를 | |
| 55 | -붙이면 이 컴포넌트를 쓰는 화면 전체가 클라이언트로 내려갑니다. | |
| 56 | - | |
| 57 | -**4. variant는 클래스 맵으로 고정한다.** | |
| 58 | - | |
| 59 | -```tsx | |
| 60 | -import styles from "./button.module.scss"; | |
| 61 | -import { cx } from "@fox/core/utils"; | |
| 62 | - | |
| 63 | -type ButtonVariant = "primary" | "secondary" | "ghost"; | |
| 64 | - | |
| 65 | -const VARIANT: Record<ButtonVariant, string> = { | |
| 66 | - primary: styles.primary, | |
| 67 | - secondary: styles.secondary, | |
| 68 | - ghost: styles.ghost, | |
| 69 | -}; | |
| 70 | - | |
| 71 | -export function Button({ variant = "primary", className, ...props }: ButtonProps) { | |
| 72 | - return <button className={cx(styles.button, VARIANT[variant], className)} {...props} />; | |
| 73 | -} | |
| 74 | -``` | |
| 75 | - | |
| 76 | -`Record<Variant, string>`이라 variant를 추가하면 맵에 항목을 빠뜨릴 수 없습니다(타입 에러). | |
| 77 | - | |
| 78 | -**5. `className` prop은 배치용으로만 열어 둔다.** | |
| 79 | - | |
| 80 | -호출부가 `className`으로 넘기는 것은 margin·grid 배치 같은 **위치** 조정이지, 색·크기 같은 | |
| 81 | -**디자인 값**이 아닙니다. 디자인이 달라져야 하면 variant를 추가합니다. | |
| 82 | - | |
| 83 | -**6. 호스트 앱에 의존하지 않는다.** | |
| 84 | - | |
| 85 | -`@fox/` 안에서는 `@/lib/...`, `@/app/...` 같은 앱 경로를 import 하지 않습니다. 이 폴더는 | |
| 86 | -다른 프로젝트로 통째로 복사되어야 하므로, 앱이 주는 값은 반드시 prop이나 CSS 변수로 | |
| 87 | -받습니다(폰트가 `--app-font-sans`를 받는 것과 같은 방식). |
--- @fox/core/utils/cx.ts
+++ @fox/core/utils/cx.ts
... | ... | @@ -1,15 +1,6 @@ |
| 1 | 1 |
export type ClassValue = string | false | null | undefined; |
| 2 | 2 |
|
| 3 |
-/** |
|
| 4 |
- * 조건부 클래스명을 합친다. falsy 값은 버린다. |
|
| 5 |
- * |
|
| 6 |
- * cx(styles.button, isActive && styles.active, className) |
|
| 7 |
- * |
|
| 8 |
- * 유틸리티 CSS 프레임워크에서 쓰는 클래스 병합 라이브러리(tailwind-merge 등)가 |
|
| 9 |
- * 필요 없는 이유: CSS Module은 클래스명이 파일 단위로 스코프되므로 서로 다른 |
|
| 10 |
- * 컴포넌트의 클래스가 같은 속성을 두고 충돌하지 않는다. 한 컴포넌트 안에서의 |
|
| 11 |
- * 충돌은 선언 순서(뒤에 온 규칙이 이김)로 결정되며, 이는 CSS의 정상 동작이다. |
|
| 12 |
- */ |
|
| 3 |
+ |
|
| 13 | 4 |
export function cx(...values: ClassValue[]): string {
|
| 14 | 5 |
return values.filter(Boolean).join(" ");
|
| 15 | 6 |
} |
--- @fox/styles/_functions.scss
... | ... | @@ -1,75 +0,0 @@ |
| 1 | -// ───────────────────────────────────────────────────────────────────────────── | |
| 2 | -// 토큰 접근 함수 — 디자인 시스템 규율을 **컴파일러가 강제**하는 지점. | |
| 3 | -// | |
| 4 | -// 화면 코드는 `color: #4f46e5`나 `padding: 13px`를 쓸 수 없고, 반드시 | |
| 5 | -// `color: fox.color(primary)` / `padding: fox.space(3)`로 토큰을 지목해야 한다. | |
| 6 | -// 존재하지 않는 이름을 쓰면 `@error`로 **빌드가 실패한다** — 오타나 임의 값이 | |
| 7 | -// 리뷰를 통과해 배포까지 흘러가는 경로가 원천 차단된다. | |
| 8 | -// | |
| 9 | -// 반환값은 CSS 커스텀 프로퍼티 참조(`var(--fox-*)`)다. 값 자체가 아니라 참조를 | |
| 10 | -// 돌려주므로 (1) 라이트/다크가 런타임에 전환되고 (2) 특정 영역만 토큰을 덮어쓰는 | |
| 11 | -// 스코프 오버라이드가 가능하며 (3) devtools에서 어떤 토큰인지 그대로 보인다. | |
| 12 | -// ───────────────────────────────────────────────────────────────────────────── | |
| 13 | - | |
| 14 | -@use "sass:map"; | |
| 15 | -@use "tokens"; | |
| 16 | - | |
| 17 | -/// 토큰 이름을 검증하고 대응하는 CSS 커스텀 프로퍼티 참조를 반환한다. | |
| 18 | -/// @param {String} $group - 토큰 그룹(커스텀 프로퍼티 접두사에 그대로 쓰인다) | |
| 19 | -/// @param {String|Number} $name - 토큰 이름 | |
| 20 | -/// @param {Map} $registry - 검증 기준이 되는 토큰 map | |
| 21 | -@function _token($group, $name, $registry) { | |
| 22 | - @if not map.has-key($registry, $name) { | |
| 23 | - @error "[@fox] 알 수 없는 #{$group} 토큰: `#{$name}` — 사용 가능한 값: #{map.keys($registry)}"; | |
| 24 | - } | |
| 25 | - | |
| 26 | - @return var(--fox-#{$group}-#{$name}); | |
| 27 | -} | |
| 28 | - | |
| 29 | -@function color($name) { | |
| 30 | - @return _token("color", $name, tokens.$color); | |
| 31 | -} | |
| 32 | - | |
| 33 | -@function space($name) { | |
| 34 | - @return _token("space", $name, tokens.$space); | |
| 35 | -} | |
| 36 | - | |
| 37 | -@function radius($name) { | |
| 38 | - @return _token("radius", $name, tokens.$radius); | |
| 39 | -} | |
| 40 | - | |
| 41 | -@function shadow($name) { | |
| 42 | - @return _token("shadow", $name, tokens.$shadow); | |
| 43 | -} | |
| 44 | - | |
| 45 | -@function duration($name) { | |
| 46 | - @return _token("duration", $name, tokens.$duration); | |
| 47 | -} | |
| 48 | - | |
| 49 | -@function easing($name) { | |
| 50 | - @return _token("easing", $name, tokens.$easing); | |
| 51 | -} | |
| 52 | - | |
| 53 | -@function font($name) { | |
| 54 | - @return _token("font", $name, tokens.$font-family); | |
| 55 | -} | |
| 56 | - | |
| 57 | -@function weight($name) { | |
| 58 | - @return _token("weight", $name, tokens.$font-weight); | |
| 59 | -} | |
| 60 | - | |
| 61 | -@function z($name) { | |
| 62 | - @return _token("z", $name, tokens.$z); | |
| 63 | -} | |
| 64 | - | |
| 65 | -/// 브레이크포인트 원값(rem 리터럴)을 반환한다 — **유일하게 `var()`가 아닌 실제 값을 | |
| 66 | -/// 돌려주는 함수**다. 미디어 쿼리가 `var()`를 해석하지 못하기 때문이다(`tokens/_layout.scss`). | |
| 67 | -/// 미디어 쿼리 자체는 `media()` 믹스인을 쓰고, 이 함수는 컨테이너 `max-width`처럼 | |
| 68 | -/// 브레이크포인트와 같은 값을 선언에 직접 써야 할 때만 쓴다. | |
| 69 | -@function bp($name) { | |
| 70 | - @if not map.has-key(tokens.$breakpoint, $name) { | |
| 71 | - @error "[@fox] 알 수 없는 breakpoint 토큰: `#{$name}` — 사용 가능한 값: #{map.keys(tokens.$breakpoint)}"; | |
| 72 | - } | |
| 73 | - | |
| 74 | - @return map.get(tokens.$breakpoint, $name); | |
| 75 | -} |
--- @fox/styles/_mixins.scss
... | ... | @@ -1,109 +0,0 @@ |
| 1 | -// ───────────────────────────────────────────────────────────────────────────── | |
| 2 | -// 믹스인 — 여러 선언이 항상 함께 가야 하는 패턴을 한 줄로 묶는다. | |
| 3 | -// | |
| 4 | -// 함수(`_functions.scss`)가 "값 하나"를 강제한다면, 믹스인은 "값들의 조합"을 | |
| 5 | -// 강제한다. 타이포 4종(크기·행간·굵기·자간)이나 감소 모션 분기처럼 하나만 빠져도 | |
| 6 | -// 어긋나는 것들을 화면마다 되풀이해 적지 않게 만드는 것이 목적이다. | |
| 7 | -// ───────────────────────────────────────────────────────────────────────────── | |
| 8 | - | |
| 9 | -@use "sass:map"; | |
| 10 | -@use "tokens"; | |
| 11 | -@use "functions" as fn; | |
| 12 | - | |
| 13 | -/// 타이포 프리셋 적용 — 크기·행간·굵기·자간 4종을 한 번에. | |
| 14 | -/// 개별 스칼라를 따로 고르지 못하게 해서 타이포 조합이 무한히 갈라지는 것을 막는다. | |
| 15 | -@mixin typo($name) { | |
| 16 | - @if not map.has-key(tokens.$typo, $name) { | |
| 17 | - @error "[@fox] 알 수 없는 typo 프리셋: `#{$name}` — 사용 가능한 값: #{map.keys(tokens.$typo)}"; | |
| 18 | - } | |
| 19 | - | |
| 20 | - font-size: var(--fox-typo-#{$name}-size); | |
| 21 | - font-weight: var(--fox-typo-#{$name}-weight); | |
| 22 | - line-height: var(--fox-typo-#{$name}-line-height); | |
| 23 | - letter-spacing: var(--fox-typo-#{$name}-letter-spacing); | |
| 24 | -} | |
| 25 | - | |
| 26 | -/// 지정 브레이크포인트 **이상**에서 적용 (모바일 우선). | |
| 27 | -@mixin media($name) { | |
| 28 | - @media (min-width: fn.bp($name)) { | |
| 29 | - @content; | |
| 30 | - } | |
| 31 | -} | |
| 32 | - | |
| 33 | -/// 지정 브레이크포인트 **미만**에서 적용. | |
| 34 | -/// `media()`와 경계가 겹치지 않도록 0.01rem을 뺀다 — 같은 뷰포트 폭에서 두 블록이 | |
| 35 | -/// 동시에 적용되면 나중 선언이 이기는 미묘한 버그가 생긴다. | |
| 36 | -@mixin media-below($name) { | |
| 37 | - @media (max-width: fn.bp($name) - 0.01rem) { | |
| 38 | - @content; | |
| 39 | - } | |
| 40 | -} | |
| 41 | - | |
| 42 | -/// 트랜지션 — `prefers-reduced-motion: no-preference`로 감싸 감소 모션 환경에서는 | |
| 43 | -/// 아예 선언되지 않게 한다(값을 0으로 만드는 것보다 확실하다). | |
| 44 | -/// @param {List} $properties - 전환할 속성. 여러 개면 괄호로 묶어 전달한다. | |
| 45 | -@mixin transition($properties, $duration: base, $easing: out) { | |
| 46 | - @media (prefers-reduced-motion: no-preference) { | |
| 47 | - transition-property: $properties; | |
| 48 | - transition-duration: fn.duration($duration); | |
| 49 | - transition-timing-function: fn.easing($easing); | |
| 50 | - } | |
| 51 | -} | |
| 52 | - | |
| 53 | -/// 키프레임 애니메이션 — 감소 모션 분기는 `transition()`과 동일하게 내장. | |
| 54 | -@mixin animate($name, $duration: base, $easing: out, $fill: both) { | |
| 55 | - @media (prefers-reduced-motion: no-preference) { | |
| 56 | - animation: $name fn.duration($duration) fn.easing($easing) $fill; | |
| 57 | - } | |
| 58 | -} | |
| 59 | - | |
| 60 | -/// 키보드 포커스 링 — `:focus-visible`만 대상으로 하므로 마우스 클릭 시에는 | |
| 61 | -/// 나타나지 않는다. 모든 상호작용 요소에 반드시 넣는다(접근성). | |
| 62 | -@mixin focus-ring($color: null, $width: 0.2rem, $offset: 0.2rem) { | |
| 63 | - $ring-color: fn.color(primary); | |
| 64 | - | |
| 65 | - @if $color != null { | |
| 66 | - $ring-color: $color; | |
| 67 | - } | |
| 68 | - | |
| 69 | - &:focus-visible { | |
| 70 | - outline: $width solid $ring-color; | |
| 71 | - outline-offset: $offset; | |
| 72 | - } | |
| 73 | -} | |
| 74 | - | |
| 75 | -/// 화면에는 보이지 않지만 스크린리더는 읽는 텍스트. | |
| 76 | -/// `display: none`이나 `visibility: hidden`은 보조기기에서도 사라지므로 쓰지 않는다. | |
| 77 | -@mixin visually-hidden { | |
| 78 | - position: absolute; | |
| 79 | - width: 1px; | |
| 80 | - height: 1px; | |
| 81 | - margin: -1px; | |
| 82 | - padding: 0; | |
| 83 | - overflow: hidden; | |
| 84 | - clip-path: inset(50%); | |
| 85 | - white-space: nowrap; | |
| 86 | - border: 0; | |
| 87 | -} | |
| 88 | - | |
| 89 | -/// 말줄임 — 1줄이면 `text-overflow`, 여러 줄이면 line-clamp. | |
| 90 | -@mixin truncate($lines: 1) { | |
| 91 | - overflow: hidden; | |
| 92 | - | |
| 93 | - @if $lines == 1 { | |
| 94 | - text-overflow: ellipsis; | |
| 95 | - white-space: nowrap; | |
| 96 | - } @else { | |
| 97 | - display: -webkit-box; | |
| 98 | - -webkit-box-orient: vertical; | |
| 99 | - -webkit-line-clamp: $lines; | |
| 100 | - } | |
| 101 | -} | |
| 102 | - | |
| 103 | -/// 페이지 콘텐츠 가로 폭 제한 + 좌우 여백. | |
| 104 | -@mixin container($max: xl, $gutter: 4) { | |
| 105 | - width: 100%; | |
| 106 | - max-width: fn.bp($max); | |
| 107 | - margin-inline: auto; | |
| 108 | - padding-inline: fn.space($gutter); | |
| 109 | -} |
--- @fox/styles/_reset.scss
... | ... | @@ -1,121 +0,0 @@ |
| 1 | -// ───────────────────────────────────────────────────────────────────────────── | |
| 2 | -// 리셋 & 베이스 | |
| 3 | -// | |
| 4 | -// 브라우저 기본 스타일 중 디자인 시스템과 충돌하는 것만 걷어낸다. 전면 리셋(모든 | |
| 5 | -// 요소를 무스타일로)이 아니라, 시맨틱 태그의 기본 동작은 살리고 시각 표현만 | |
| 6 | -// 토큰으로 되돌리는 방식이다. | |
| 7 | -// ───────────────────────────────────────────────────────────────────────────── | |
| 8 | - | |
| 9 | -@use "abstracts" as fox; | |
| 10 | - | |
| 11 | -*, | |
| 12 | -*::before, | |
| 13 | -*::after { | |
| 14 | - box-sizing: border-box; | |
| 15 | -} | |
| 16 | - | |
| 17 | -// ⚠️ **1rem = 10px 규약의 근거.** 브라우저 기본 글자 크기(16px)의 62.5%가 10px다. | |
| 18 | -// px → rem 환산이 10으로 나누기가 되어 토큰 값이 읽기 쉬워진다. | |
| 19 | -// px 고정과 달리 사용자가 브라우저 글자 크기를 키우면 앱 전체가 비례해 확대된다 | |
| 20 | -// (접근성 설정 존중). 그 대신 `body`에서 실제 본문 크기를 되돌려 놓아야 한다 — | |
| 21 | -// 아래 `body`의 `typo(body-md)`가 그 역할이다. | |
| 22 | -// | |
| 23 | -// ⚠️ 미디어 쿼리 안의 `rem`은 이 값의 영향을 받지 않는다(항상 브라우저 기본 크기 | |
| 24 | -// 기준). `tokens/_layout.scss` 참고. | |
| 25 | -html { | |
| 26 | - font-size: 62.5%; | |
| 27 | - -webkit-text-size-adjust: 100%; | |
| 28 | -} | |
| 29 | - | |
| 30 | -body { | |
| 31 | - margin: 0; | |
| 32 | - background-color: fox.color(background); | |
| 33 | - color: fox.color(foreground); | |
| 34 | - font-family: fox.font(sans); | |
| 35 | - -webkit-font-smoothing: antialiased; | |
| 36 | - -moz-osx-font-smoothing: grayscale; | |
| 37 | - | |
| 38 | - @include fox.typo(body-md); | |
| 39 | -} | |
| 40 | - | |
| 41 | -// 제목·문단의 기본 여백을 제거한다 — 간격은 레이아웃(gap/margin 토큰)이 책임지고, | |
| 42 | -// 태그가 스스로 여백을 갖지 않게 해야 조립 결과를 예측할 수 있다. | |
| 43 | -h1, | |
| 44 | -h2, | |
| 45 | -h3, | |
| 46 | -h4, | |
| 47 | -h5, | |
| 48 | -h6, | |
| 49 | -p, | |
| 50 | -figure, | |
| 51 | -blockquote, | |
| 52 | -dl, | |
| 53 | -dd { | |
| 54 | - margin: 0; | |
| 55 | -} | |
| 56 | - | |
| 57 | -// 제목 태그의 기본 크기/굵기도 제거한다 — 시각적 위계는 `typo()` 프리셋으로만 | |
| 58 | -// 정하고, 태그 선택은 문서 구조(접근성) 기준으로 하게 만든다. | |
| 59 | -h1, | |
| 60 | -h2, | |
| 61 | -h3, | |
| 62 | -h4, | |
| 63 | -h5, | |
| 64 | -h6 { | |
| 65 | - font-size: inherit; | |
| 66 | - font-weight: inherit; | |
| 67 | -} | |
| 68 | - | |
| 69 | -ul, | |
| 70 | -ol { | |
| 71 | - margin: 0; | |
| 72 | - padding: 0; | |
| 73 | - list-style: none; | |
| 74 | -} | |
| 75 | - | |
| 76 | -// 폼 요소는 폰트를 상속하지 않는 것이 브라우저 기본값이라 명시적으로 되돌린다. | |
| 77 | -button, | |
| 78 | -input, | |
| 79 | -select, | |
| 80 | -textarea { | |
| 81 | - margin: 0; | |
| 82 | - font: inherit; | |
| 83 | - color: inherit; | |
| 84 | - letter-spacing: inherit; | |
| 85 | -} | |
| 86 | - | |
| 87 | -button { | |
| 88 | - padding: 0; | |
| 89 | - border: 0; | |
| 90 | - background: none; | |
| 91 | - cursor: pointer; | |
| 92 | -} | |
| 93 | - | |
| 94 | -button:disabled { | |
| 95 | - cursor: default; | |
| 96 | -} | |
| 97 | - | |
| 98 | -a { | |
| 99 | - color: inherit; | |
| 100 | - text-decoration: none; | |
| 101 | -} | |
| 102 | - | |
| 103 | -img, | |
| 104 | -svg, | |
| 105 | -video, | |
| 106 | -canvas { | |
| 107 | - display: block; | |
| 108 | - max-width: 100%; | |
| 109 | -} | |
| 110 | - | |
| 111 | -table { | |
| 112 | - border-collapse: collapse; | |
| 113 | - border-spacing: 0; | |
| 114 | -} | |
| 115 | - | |
| 116 | -// 브라우저 기본 포커스 링을 지우지 않는다 — 컴포넌트가 `focus-ring()` 믹스인으로 | |
| 117 | -// 자기 링을 정의하면 그것이 우선하고, 정의하지 않은 요소는 기본 링이 남아 키보드 | |
| 118 | -// 사용자가 길을 잃지 않는다. | |
| 119 | -:where(:focus-visible) { | |
| 120 | - outline-offset: 0.2rem; | |
| 121 | -} |
--- @fox/styles/_root.scss
... | ... | @@ -1,104 +0,0 @@ |
| 1 | -// ───────────────────────────────────────────────────────────────────────────── | |
| 2 | -// 토큰 CSS 출력 — `tokens/` 의 SCSS map을 `--fox-*` 커스텀 프로퍼티로 선언한다. | |
| 3 | -// | |
| 4 | -// **이 파일은 앱 전체에서 딱 한 번만 로드되어야 한다** (`index.scss` 경유). | |
| 5 | -// 컴포넌트의 `.module.scss`에서는 절대 `@use` 하지 않는다 — CSS Module은 파일마다 | |
| 6 | -// 별도 컴파일 단위여서, 컴포넌트마다 `:root` 블록 전체가 복제된다. | |
| 7 | -// | |
| 8 | -// map을 순회해 생성하므로 토큰을 추가할 때 이 파일은 손대지 않는다. 값의 이름·개수는 | |
| 9 | -// 언제나 `tokens/` 쪽 map 하나에만 존재한다. | |
| 10 | -// ───────────────────────────────────────────────────────────────────────────── | |
| 11 | - | |
| 12 | -@use "sass:map"; | |
| 13 | -@use "tokens"; | |
| 14 | - | |
| 15 | -:root { | |
| 16 | - // 기본은 OS 설정 추종. 아래 `[data-theme]` 규칙이 이 값만 덮어써 수동 선택을 | |
| 17 | - // 처리하므로, 색상 값 자체는 `tokens/_color.scss` 한 곳에만 존재한다. | |
| 18 | - color-scheme: light dark; | |
| 19 | - | |
| 20 | - @each $name, $value in tokens.$color { | |
| 21 | - --fox-color-#{$name}: #{$value}; | |
| 22 | - } | |
| 23 | - | |
| 24 | - @each $name, $value in tokens.$space { | |
| 25 | - --fox-space-#{$name}: #{$value}; | |
| 26 | - } | |
| 27 | - | |
| 28 | - @each $name, $value in tokens.$radius { | |
| 29 | - --fox-radius-#{$name}: #{$value}; | |
| 30 | - } | |
| 31 | - | |
| 32 | - @each $name, $value in tokens.$shadow { | |
| 33 | - --fox-shadow-#{$name}: #{$value}; | |
| 34 | - } | |
| 35 | - | |
| 36 | - @each $name, $value in tokens.$duration { | |
| 37 | - --fox-duration-#{$name}: #{$value}; | |
| 38 | - } | |
| 39 | - | |
| 40 | - @each $name, $value in tokens.$easing { | |
| 41 | - --fox-easing-#{$name}: #{$value}; | |
| 42 | - } | |
| 43 | - | |
| 44 | - @each $name, $value in tokens.$font-family { | |
| 45 | - --fox-font-#{$name}: #{$value}; | |
| 46 | - } | |
| 47 | - | |
| 48 | - @each $name, $value in tokens.$font-weight { | |
| 49 | - --fox-weight-#{$name}: #{$value}; | |
| 50 | - } | |
| 51 | - | |
| 52 | - @each $name, $value in tokens.$z { | |
| 53 | - --fox-z-#{$name}: #{$value}; | |
| 54 | - } | |
| 55 | - | |
| 56 | - // 브레이크포인트는 **미디어 쿼리에서 쓸 수 없다** — 조건부는 `var()`를 해석하지 | |
| 57 | - // 않는다. 그럼에도 내보내는 이유는 컨테이너 `max-width`처럼 선언부에서 같은 값을 | |
| 58 | - // 참조할 일이 있고, 카탈로그가 토큰 목록을 CSSOM에서 읽기 때문이다. | |
| 59 | - // 미디어 쿼리는 언제나 `fox.media()` 믹스인을 쓴다. | |
| 60 | - @each $name, $value in tokens.$breakpoint { | |
| 61 | - --fox-breakpoint-#{$name}: #{$value}; | |
| 62 | - } | |
| 63 | - | |
| 64 | - // 타이포 프리셋은 한 이름당 4개의 프로퍼티로 펼쳐진다 — `fox.typo()` 믹스인이 | |
| 65 | - // 이 네 개를 함께 참조한다. | |
| 66 | - @each $name, $preset in tokens.$typo { | |
| 67 | - --fox-typo-#{$name}-size: #{map.get($preset, size)}; | |
| 68 | - --fox-typo-#{$name}-weight: #{map.get($preset, weight)}; | |
| 69 | - --fox-typo-#{$name}-line-height: #{map.get($preset, line-height)}; | |
| 70 | - --fox-typo-#{$name}-letter-spacing: #{map.get($preset, letter-spacing)}; | |
| 71 | - } | |
| 72 | -} | |
| 73 | - | |
| 74 | -// 수동 테마 선택 — `color-scheme`만 고정하면 `light-dark()`가 해당 분기로 확정된다. | |
| 75 | -// 색상 값을 여기서 재정의하지 않으므로 라이트/다크 값이 어긋날 수 없다. | |
| 76 | -// 속성이 없거나 `"system"`이면 위 `:root`의 `light dark`가 유지되어 OS를 따른다. | |
| 77 | -:root[data-theme="light"] { | |
| 78 | - color-scheme: light; | |
| 79 | -} | |
| 80 | - | |
| 81 | -:root[data-theme="dark"] { | |
| 82 | - color-scheme: dark; | |
| 83 | -} | |
| 84 | - | |
| 85 | -// 테마 전환 애니메이션 — 호스트 앱이 토글 직전 `<html>`에 `[data-theme-transition]`을 | |
| 86 | -// 부여하고 ~200ms 후 제거하는 **시간 한정 opt-in**이다. 상시 트랜지션이 아니므로 | |
| 87 | -// 다른 hover 트랜지션과 경쟁하지 않는다. | |
| 88 | -// `prefers-reduced-motion` 게이팅으로 감소 모션 환경에서는 즉시 반영된다. | |
| 89 | -// `!important`는 이 200ms 동안만 존재하는 규칙이 컴포넌트의 transition 선언에 | |
| 90 | -// 밀리지 않게 하기 위한 것이다. | |
| 91 | -@media (prefers-reduced-motion: no-preference) { | |
| 92 | - html[data-theme-transition], | |
| 93 | - html[data-theme-transition] *, | |
| 94 | - html[data-theme-transition] *::before, | |
| 95 | - html[data-theme-transition] *::after { | |
| 96 | - transition: | |
| 97 | - background-color var(--fox-duration-base) var(--fox-easing-out), | |
| 98 | - border-color var(--fox-duration-base) var(--fox-easing-out), | |
| 99 | - color var(--fox-duration-base) var(--fox-easing-out), | |
| 100 | - fill var(--fox-duration-base) var(--fox-easing-out), | |
| 101 | - stroke var(--fox-duration-base) var(--fox-easing-out), | |
| 102 | - outline-color var(--fox-duration-base) var(--fox-easing-out) !important; | |
| 103 | - } | |
| 104 | -} |
--- @fox/styles/abstracts.scss
... | ... | @@ -1,22 +0,0 @@ |
| 1 | -// ───────────────────────────────────────────────────────────────────────────── | |
| 2 | -// 저작(authoring) 진입점 — 컴포넌트의 `.module.scss`가 `@use`하는 파일. | |
| 3 | -// | |
| 4 | -// @use "@fox/styles/abstracts" as fox; | |
| 5 | -// | |
| 6 | -// .button { | |
| 7 | -// padding: fox.space(2) fox.space(4); | |
| 8 | -// background: fox.color(primary); | |
| 9 | -// border-radius: fox.radius(md); | |
| 10 | -// @include fox.typo(label-lg); | |
| 11 | -// @include fox.focus-ring; | |
| 12 | -// } | |
| 13 | -// | |
| 14 | -// ⚠️ **이 파일은 CSS를 한 줄도 출력하지 않는다.** 함수·믹스인·map 정의만 있다. | |
| 15 | -// CSS Module은 파일마다 별도 컴파일 단위라, CSS를 출력하는 모듈을 여기서 forward하면 | |
| 16 | -// 컴포넌트 수만큼 같은 CSS가 복제된다. 실제 CSS 출력(토큰 선언·리셋)은 앱이 딱 한 번 | |
| 17 | -// import하는 `index.scss`가 전담한다. | |
| 18 | -// ───────────────────────────────────────────────────────────────────────────── | |
| 19 | - | |
| 20 | -@forward "tokens"; | |
| 21 | -@forward "functions"; | |
| 22 | -@forward "mixins"; |
--- @fox/styles/index.scss
+++ @fox/styles/index.scss
... | ... | @@ -1,13 +1,0 @@ |
| 1 |
-// ───────────────────────────────────────────────────────────────────────────── |
|
| 2 |
-// 글로벌 스타일 진입점 — **호스트 앱이 딱 한 번 import한다.** |
|
| 3 |
-// |
|
| 4 |
-// // app/globals.scss |
|
| 5 |
-// @use "@fox/styles"; |
|
| 6 |
-// |
|
| 7 |
-// 여기서만 CSS가 출력된다(토큰 선언 + 리셋). 컴포넌트의 `.module.scss`는 이 파일이 |
|
| 8 |
-// 아니라 `abstracts.scss`를 `@use`해야 한다 — 그쪽은 CSS 출력이 0이라 몇 번을 |
|
| 9 |
-// 참조해도 중복이 생기지 않는다. |
|
| 10 |
-// ───────────────────────────────────────────────────────────────────────────── |
|
| 11 |
- |
|
| 12 |
-@use "root"; |
|
| 13 |
-@use "reset"; |
--- @fox/styles/tokens/_color.scss
... | ... | @@ -1,61 +0,0 @@ |
| 1 | -// ───────────────────────────────────────────────────────────────────────────── | |
| 2 | -// 색상 토큰 | |
| 3 | -// | |
| 4 | -// 이 map이 단일 진실 공급원(SSOT)이다. 여기서 두 가지가 함께 파생된다: | |
| 5 | -// 1. `_root.scss` → `--fox-color-*` CSS 커스텀 프로퍼티 선언 | |
| 6 | -// 2. `_functions.scss`의 `color()` → 토큰 이름 유효성 컴파일타임 검증 | |
| 7 | -// 값과 이름 목록이 한 파일에만 존재하므로 둘 사이의 드리프트가 구조적으로 불가능하다. | |
| 8 | -// | |
| 9 | -// 라이트/다크는 `light-dark(라이트값, 다크값)` 한 줄로 함께 보유한다. 테마 전환은 | |
| 10 | -// `_root.scss`의 `color-scheme`이 담당하므로 다크 전용 블록을 따로 두지 않는다 — | |
| 11 | -// 한쪽만 고쳐서 생기는 테마 불일치가 애초에 발생할 수 없다. | |
| 12 | -// | |
| 13 | -// 브라우저 지원: `light-dark()` 자체는 Chrome 123 / Safari 17.5 / Firefox 120 이상이 | |
| 14 | -// 필요하지만, Next.js 16(Turbopack)의 Lightning CSS가 현재 설정에서 이를 | |
| 15 | -// `var(--lightningcss-light, 라이트) var(--lightningcss-dark, 다크)` 조합으로 **폴리필**해 | |
| 16 | -// 출력하므로 구형 브라우저에서도 동작한다(2026-08-11 빌드 산출물에서 확인). | |
| 17 | -// browserslist 타깃을 최신 브라우저로 좁히면 폴리필이 빠지고 원본 `light-dark()`가 | |
| 18 | -// 그대로 나가므로, 타깃을 바꿀 때 이 전제를 다시 확인한다. | |
| 19 | -// | |
| 20 | -// ⚠️ 아래 값은 전부 **임시 플레이스홀더**다. 파이프라인 검증과 /design 카탈로그 | |
| 21 | -// 렌더를 위한 중립 팔레트이며, 실제 브랜드 값은 사용자 제공 Figma/파일로 교체한다. | |
| 22 | -// 교체 시 이 파일의 값만 바꾸면 앱 전체에 반영된다. | |
| 23 | -// ───────────────────────────────────────────────────────────────────────────── | |
| 24 | - | |
| 25 | -$color: ( | |
| 26 | - // 브랜드 | |
| 27 | - primary: light-dark(#4f46e5, #818cf8), | |
| 28 | - primary-hover: light-dark(#4338ca, #a5b4fc), | |
| 29 | - primary-subtle: light-dark(#eef2ff, #312e81), | |
| 30 | - on-primary: light-dark(#ffffff, #1e1b34), | |
| 31 | - | |
| 32 | - // 표면 — background(페이지 바닥) < surface(카드) < surface-raised(팝오버 등) | |
| 33 | - background: light-dark(#ffffff, #14161b), | |
| 34 | - surface: light-dark(#ffffff, #1e2128), | |
| 35 | - surface-muted: light-dark(#f5f7fa, #262a33), | |
| 36 | - surface-raised: light-dark(#ffffff, #2c3038), | |
| 37 | - | |
| 38 | - // 전경(텍스트·아이콘) | |
| 39 | - foreground: light-dark(#1f2430, #f5f7fa), | |
| 40 | - foreground-muted: light-dark(#565d6b, #bbc4d8), | |
| 41 | - foreground-subtle: light-dark(#8790a3, #8790a3), | |
| 42 | - foreground-inverse: light-dark(#ffffff, #14161b), | |
| 43 | - | |
| 44 | - // 경계선 | |
| 45 | - border: light-dark(#dae0ed, #424752), | |
| 46 | - border-subtle: light-dark(#eaeef6, #2f343d), | |
| 47 | - border-strong: light-dark(#b6bfd2, #5a616e), | |
| 48 | - | |
| 49 | - // 상태 — 각 상태색 위에 얹는 텍스트색을 on-* 로 함께 정의한다. | |
| 50 | - danger: light-dark(#dc2626, #f87171), | |
| 51 | - on-danger: light-dark(#ffffff, #1a0d0d), | |
| 52 | - success: light-dark(#16a34a, #4ade80), | |
| 53 | - on-success: light-dark(#ffffff, #0a1a10), | |
| 54 | - warning: light-dark(#d97706, #fbbf24), | |
| 55 | - on-warning: light-dark(#ffffff, #1a1205), | |
| 56 | - info: light-dark(#0284c7, #38bdf8), | |
| 57 | - on-info: light-dark(#ffffff, #05161f), | |
| 58 | - | |
| 59 | - // 오버레이 | |
| 60 | - scrim: light-dark(rgb(0 0 0 / 0.5), rgb(0 0 0 / 0.65)) | |
| 61 | -); |
--- @fox/styles/tokens/_index.scss
... | ... | @@ -1,13 +0,0 @@ |
| 1 | -// 토큰 map 모음 진입점 — **CSS를 한 줄도 출력하지 않는다.** | |
| 2 | -// 값 선언(이 폴더)과 CSS 출력(`../_root.scss`)을 분리해 두었기 때문에, 컴포넌트의 | |
| 3 | -// `.module.scss`가 몇 개든 `abstracts`를 `@use`해도 `:root` 블록이 중복 출력되지 | |
| 4 | -// 않는다(각 CSS Module은 별도 컴파일 단위라 CSS를 내보내는 모듈을 공유하면 | |
| 5 | -// 파일마다 같은 CSS가 복제된다). | |
| 6 | - | |
| 7 | -@forward "color"; | |
| 8 | -@forward "space"; | |
| 9 | -@forward "typography"; | |
| 10 | -@forward "radius"; | |
| 11 | -@forward "shadow"; | |
| 12 | -@forward "motion"; | |
| 13 | -@forward "layout"; |
--- @fox/styles/tokens/_layout.scss
... | ... | @@ -1,41 +0,0 @@ |
| 1 | -// ───────────────────────────────────────────────────────────────────────────── | |
| 2 | -// 레이아웃 토큰 — 브레이크포인트와 z-index 층. | |
| 3 | -// ───────────────────────────────────────────────────────────────────────────── | |
| 4 | - | |
| 5 | -// 브레이크포인트는 이 파일에서 **유일하게 CSS 커스텀 프로퍼티로 내보내지 않는 | |
| 6 | -// 토큰**이다. 미디어 쿼리 조건부는 `var()`를 해석하지 않기 때문에 | |
| 7 | -// (`@media (min-width: var(--x))`는 무효), 컴파일타임 SCSS 값이어야만 한다. | |
| 8 | -// 그래서 브레이크포인트 사용은 `fox.media()` 믹스인 경유가 강제된다. | |
| 9 | -// | |
| 10 | -// ⚠️ **미디어 쿼리 안의 `rem`은 브라우저 기본 글자 크기(보통 16px) 기준이다.** | |
| 11 | -// `html { font-size: 62.5% }`의 영향을 받지 않으므로, 다른 토큰의 "1rem = 10px" | |
| 12 | -// 규칙이 여기서는 적용되지 않는다. 아래 px 환산을 반드시 함께 본다. | |
| 13 | -// rem으로 두는 이유: 사용자가 브라우저 글자 크기를 키우면 브레이크포인트도 함께 | |
| 14 | -// 커져 확대 환경에서 레이아웃이 더 일찍 접힌다(px 고정은 이 대응이 불가능하다). | |
| 15 | -$breakpoint: ( | |
| 16 | - sm: 40rem, | |
| 17 | - // 640px | |
| 18 | - md: 48rem, | |
| 19 | - // 768px | |
| 20 | - lg: 64rem, | |
| 21 | - // 1024px | |
| 22 | - xl: 80rem, | |
| 23 | - // 1280px | |
| 24 | - 2xl: 96rem | |
| 25 | - // 1536px | |
| 26 | -); | |
| 27 | - | |
| 28 | -// z-index 층 — 숫자를 화면에서 직접 고르지 않게 하려는 토큰이다. 값 사이를 100씩 | |
| 29 | -// 띄워 둔 것은 나중에 층 사이에 새 층을 끼워 넣을 여지를 남기기 위함이다. | |
| 30 | -$z: ( | |
| 31 | - base: 0, | |
| 32 | - raised: 100, | |
| 33 | - sticky: 200, | |
| 34 | - dropdown: 300, | |
| 35 | - scrim: 400, | |
| 36 | - modal: 500, | |
| 37 | - popover: 600, | |
| 38 | - toast: 700, | |
| 39 | - tooltip: 800, | |
| 40 | - splash: 900 | |
| 41 | -); |
--- @fox/styles/tokens/_motion.scss
... | ... | @@ -1,26 +0,0 @@ |
| 1 | -// ───────────────────────────────────────────────────────────────────────────── | |
| 2 | -// 모션 토큰 — 지속시간과 이징. | |
| 3 | -// 이징은 "어느 방향으로 움직이는가"로 이름 붙인다: 화면에 들어올 때는 감속(out), | |
| 4 | -// 나갈 때는 가속(in), 제자리에서 상태만 바뀔 때는 in-out. | |
| 5 | -// | |
| 6 | -// 실제 트랜지션·애니메이션은 `_mixins.scss`의 `transition()` / `animate()`를 쓴다 — | |
| 7 | -// 두 믹스인 모두 `prefers-reduced-motion` 분기를 내장하고 있어 감소 모션 환경 | |
| 8 | -// 대응을 화면마다 되풀이할 필요가 없다. | |
| 9 | -// ───────────────────────────────────────────────────────────────────────────── | |
| 10 | - | |
| 11 | -$duration: ( | |
| 12 | - instant: 0ms, | |
| 13 | - fast: 120ms, | |
| 14 | - base: 200ms, | |
| 15 | - slow: 300ms, | |
| 16 | - slower: 500ms | |
| 17 | -); | |
| 18 | - | |
| 19 | -$easing: ( | |
| 20 | - linear: linear, | |
| 21 | - in: cubic-bezier(0.4, 0, 1, 1), | |
| 22 | - out: cubic-bezier(0, 0, 0.2, 1), | |
| 23 | - in-out: cubic-bezier(0.4, 0, 0.2, 1), | |
| 24 | - // 살짝 오버슈트 — 토글·체크 같은 작은 확인 동작에. | |
| 25 | - spring: cubic-bezier(0.34, 1.56, 0.64, 1) | |
| 26 | -); |
--- @fox/styles/tokens/_radius.scss
... | ... | @@ -1,17 +0,0 @@ |
| 1 | -// ───────────────────────────────────────────────────────────────────────────── | |
| 2 | -// 모서리 반경 토큰 — 1rem = 10px 기준. | |
| 3 | -// `full`은 pill 형태(캡슐 버튼·뱃지)를 위한 값으로, 높이보다 크기만 하면 되므로 | |
| 4 | -// 임의의 큰 값을 쓴다. | |
| 5 | -// ⚠️ 값은 임시 플레이스홀더 — Figma 확정 시 교체. | |
| 6 | -// ───────────────────────────────────────────────────────────────────────────── | |
| 7 | - | |
| 8 | -$radius: ( | |
| 9 | - none: 0, | |
| 10 | - xs: 0.4rem, | |
| 11 | - sm: 0.6rem, | |
| 12 | - md: 0.8rem, | |
| 13 | - lg: 1.2rem, | |
| 14 | - xl: 1.6rem, | |
| 15 | - 2xl: 2.4rem, | |
| 16 | - full: 999rem | |
| 17 | -); |
--- @fox/styles/tokens/_shadow.scss
... | ... | @@ -1,18 +0,0 @@ |
| 1 | -// ───────────────────────────────────────────────────────────────────────────── | |
| 2 | -// 그림자 토큰 — 고도(elevation)를 역할 이름으로 표현한다. | |
| 3 | -// 크기(sm/md/lg)가 아니라 쓰임(card/dialog/…)으로 이름 붙이면 "이 요소는 어느 | |
| 4 | -// 층에 뜨는가"가 코드에 드러나고, 나중에 값이 바뀌어도 의미가 유지된다. | |
| 5 | -// | |
| 6 | -// 그림자는 라이트/다크에서 같은 값을 쓰지 않는 편이 자연스럽다(어두운 배경에서는 | |
| 7 | -// 그림자가 거의 보이지 않는다). 필요해지면 여기서 `light-dark()`로 분기한다. | |
| 8 | -// ⚠️ 값은 임시 플레이스홀더 — Figma 확정 시 교체. | |
| 9 | -// ───────────────────────────────────────────────────────────────────────────── | |
| 10 | - | |
| 11 | -$shadow: ( | |
| 12 | - none: none, | |
| 13 | - card: light-dark(0 0.1rem 0.3rem rgb(16 24 40 / 0.08), 0 0.1rem 0.3rem rgb(0 0 0 / 0.4)), | |
| 14 | - raised: light-dark(0 0.4rem 0.8rem rgb(16 24 40 / 0.1), 0 0.4rem 0.8rem rgb(0 0 0 / 0.45)), | |
| 15 | - popup: light-dark(0 0.8rem 1.6rem rgb(16 24 40 / 0.12), 0 0.8rem 1.6rem rgb(0 0 0 / 0.5)), | |
| 16 | - dialog: light-dark(0 1.6rem 3.2rem rgb(16 24 40 / 0.16), 0 1.6rem 3.2rem rgb(0 0 0 / 0.55)), | |
| 17 | - toast: light-dark(0 1rem 2rem rgb(16 24 40 / 0.18), 0 1rem 2rem rgb(0 0 0 / 0.6)) | |
| 18 | -); |
--- @fox/styles/tokens/_space.scss
... | ... | @@ -1,29 +0,0 @@ |
| 1 | -// ───────────────────────────────────────────────────────────────────────────── | |
| 2 | -// 간격 토큰 (4/8 grid) | |
| 3 | -// | |
| 4 | -// **모든 치수는 rem 기준이며 `1rem = 10px`이다** (`_reset.scss`의 | |
| 5 | -// `html { font-size: 62.5% }`). px → rem 환산이 10으로 나누기라 암산이 되고, | |
| 6 | -// 사용자가 브라우저 기본 글자 크기를 키우면 레이아웃 전체가 함께 확대된다 | |
| 7 | -// (px 고정과 달리 접근성 설정을 존중한다). | |
| 8 | -// | |
| 9 | -// 키는 4px 배수 단위다 — `space(4)` = 16px. 임의 값이 필요해 보이면 스케일에 | |
| 10 | -// 단계를 추가할 일인지 먼저 검토한다(§10.4). | |
| 11 | -// ───────────────────────────────────────────────────────────────────────────── | |
| 12 | - | |
| 13 | -$space: ( | |
| 14 | - 0: 0, | |
| 15 | - 1: 0.4rem, | |
| 16 | - 2: 0.8rem, | |
| 17 | - 3: 1.2rem, | |
| 18 | - 4: 1.6rem, | |
| 19 | - 5: 2rem, | |
| 20 | - 6: 2.4rem, | |
| 21 | - 8: 3.2rem, | |
| 22 | - 10: 4rem, | |
| 23 | - 12: 4.8rem, | |
| 24 | - 16: 6.4rem, | |
| 25 | - 20: 8rem, | |
| 26 | - 24: 9.6rem | |
| 27 | -); | |
| 28 | -// 키 → px 환산: 1=4 · 2=8 · 3=12 · 4=16 · 5=20 · 6=24 · 8=32 · 10=40 · 12=48 · | |
| 29 | -// 16=64 · 20=80 · 24=96 |
--- @fox/styles/tokens/_typography.scss
... | ... | @@ -1,52 +0,0 @@ |
| 1 | -// ───────────────────────────────────────────────────────────────────────────── | |
| 2 | -// 타이포그래피 토큰 | |
| 3 | -// | |
| 4 | -// 폰트 패밀리는 **호스트 앱이 주입하는 계약**이다 — `@fox`는 `--app-font-sans` / | |
| 5 | -// `--app-font-mono`를 참조만 하고 폰트 파일을 소유하지 않는다. Next.js는 `next/font`가 | |
| 6 | -// 앱의 layout에서 CSS 변수를 만들어 주므로(폰트 최적화·자체 호스팅이 앱 책임), | |
| 7 | -// `@fox` 폴더를 다른 프로젝트로 복사해도 그 프로젝트의 폰트를 그대로 따른다. | |
| 8 | -// 미주입 시에도 깨지지 않도록 `var()` 폴백을 반드시 둔다 — 폴백 없는 `var()`가 | |
| 9 | -// undefined면 font-family 선언 전체가 무효가 된다. | |
| 10 | -// | |
| 11 | -// 개별 스칼라(size/weight/…)보다 **프리셋(`$typo`)을 우선 사용**한다. 크기·행간· | |
| 12 | -// 자간·굵기는 함께 움직여야 의도한 타이포가 되므로, 화면에서 넷을 따로 고르게 두면 | |
| 13 | -// 조합이 무한히 갈라진다. `@include fox.typo(title-md)` 한 줄로 넷이 함께 적용된다. | |
| 14 | -// | |
| 15 | -// ⚠️ 아래 값·이름은 임시 플레이스홀더다. 실제 타입 스케일은 Figma 확정 시 교체한다. | |
| 16 | -// ───────────────────────────────────────────────────────────────────────────── | |
| 17 | - | |
| 18 | -$font-family: ( | |
| 19 | - sans: (var(--app-font-sans, system-ui), "Apple SD Gothic Neo", sans-serif), | |
| 20 | - mono: (var(--app-font-mono, ui-monospace), monospace) | |
| 21 | -); | |
| 22 | - | |
| 23 | -$font-weight: ( | |
| 24 | - regular: 400, | |
| 25 | - medium: 500, | |
| 26 | - semibold: 600, | |
| 27 | - bold: 700 | |
| 28 | -); | |
| 29 | - | |
| 30 | -// 타이포 프리셋 — size / line-height / weight / letter-spacing 4종을 한 묶음으로. | |
| 31 | -// 1rem = 10px 기준이므로 1.6rem = 16px. | |
| 32 | -$typo: ( | |
| 33 | - display-lg: (size: 4.8rem, line-height: 1.2, weight: 700, letter-spacing: -0.02em), | |
| 34 | - display-md: (size: 4rem, line-height: 1.2, weight: 700, letter-spacing: -0.02em), | |
| 35 | - display-sm: (size: 3.2rem, line-height: 1.25, weight: 700, letter-spacing: -0.02em), | |
| 36 | - | |
| 37 | - headline-lg: (size: 2.8rem, line-height: 1.3, weight: 700, letter-spacing: -0.015em), | |
| 38 | - headline-md: (size: 2.4rem, line-height: 1.35, weight: 700, letter-spacing: -0.015em), | |
| 39 | - headline-sm: (size: 2rem, line-height: 1.4, weight: 700, letter-spacing: -0.01em), | |
| 40 | - | |
| 41 | - title-lg: (size: 1.8rem, line-height: 1.45, weight: 600, letter-spacing: -0.01em), | |
| 42 | - title-md: (size: 1.6rem, line-height: 1.5, weight: 600, letter-spacing: 0), | |
| 43 | - title-sm: (size: 1.4rem, line-height: 1.5, weight: 600, letter-spacing: 0), | |
| 44 | - | |
| 45 | - body-lg: (size: 1.6rem, line-height: 1.6, weight: 400, letter-spacing: 0), | |
| 46 | - body-md: (size: 1.4rem, line-height: 1.6, weight: 400, letter-spacing: 0), | |
| 47 | - body-sm: (size: 1.2rem, line-height: 1.55, weight: 400, letter-spacing: 0), | |
| 48 | - | |
| 49 | - label-lg: (size: 1.4rem, line-height: 1.4, weight: 600, letter-spacing: 0), | |
| 50 | - label-md: (size: 1.3rem, line-height: 1.4, weight: 500, letter-spacing: 0), | |
| 51 | - label-sm: (size: 1.2rem, line-height: 1.35, weight: 500, letter-spacing: 0.01em) | |
| 52 | -); |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?