Merge branch 'claude/design-system-setup-46d8aa' into hub
Figma 토큰 531개 이식 + JSON→SCSS 변환기 + 카탈로그 재작성. 빈 껍데기였던 @fox/styles가 실제 토큰으로 채워져 빌드가 복구된다. Co-Authored-By: Claude Opus 5
@0caa9e1f783b6eabf5638481b1375b22a5a6e81d
+++ @fox/styles/_functions.scss
... | ... | @@ -0,0 +1,103 @@ |
| 1 | +// 토큰 접근 함수 — 디자인 시스템 규율을 **컴파일러가 강제**하는 지점. | |
| 2 | +// | |
| 3 | +// 화면 코드는 `color: #256EF4`나 `padding: 13px`를 쓸 수 없고, 반드시 | |
| 4 | +// `color: fox.color(font-neutral-strong)` / `padding: fox.padding(6)`로 토큰을 | |
| 5 | +// 지목해야 한다. 존재하지 않는 이름을 쓰면 `@error`로 **빌드가 실패**하고 사용 | |
| 6 | +// 가능한 토큰 목록이 함께 출력된다. | |
| 7 | +// | |
| 8 | +// 반환값은 CSS 커스텀 프로퍼티 참조(`var(--fox-*)`)다. 값이 아니라 참조를 돌려주므로 | |
| 9 | +// (1) 라이트/다크와 pc/모바일이 런타임에 전환되고 (2) 특정 영역만 토큰을 덮어쓰는 | |
| 10 | +// 스코프 오버라이드가 가능하며 (3) devtools에서 어떤 토큰인지 그대로 보인다. | |
| 11 | + | |
| 12 | +@use "sass:map"; | |
| 13 | +@use "tokens"; | |
| 14 | + | |
| 15 | +/// 토큰 이름을 검증하고 커스텀 프로퍼티 참조를 반환한다. | |
| 16 | +@function _token($group, $key, $registry, $var-name) { | |
| 17 | + @if not map.has-key($registry, $key) { | |
| 18 | + @error "[@fox] 알 수 없는 #{$group} 토큰: `#{$key}` — 사용 가능한 값: #{map.keys($registry)}"; | |
| 19 | + } | |
| 20 | + | |
| 21 | + @return var(--fox-#{$var-name}); | |
| 22 | +} | |
| 23 | + | |
| 24 | + | |
| 25 | +@function color($name) { | |
| 26 | + @return _token("color", $name, tokens.$color, "color-#{$name}"); | |
| 27 | +} | |
| 28 | + | |
| 29 | +@function primitive($name) { | |
| 30 | + @return _token("primitive", $name, tokens.$primitive, "primitive-#{$name}"); | |
| 31 | +} | |
| 32 | + | |
| 33 | +@function theme($name) { | |
| 34 | + @return _token("theme", $name, tokens.$theme, "theme-#{$name}"); | |
| 35 | +} | |
| 36 | + | |
| 37 | +@function number($name) { | |
| 38 | + @return _token("number", $name, tokens.$number, "number-#{$name}"); | |
| 39 | +} | |
| 40 | + | |
| 41 | +@function font-family($name) { | |
| 42 | + @return _token("font-family", "family-#{$name}", tokens.$font, "font-family-#{$name}"); | |
| 43 | +} | |
| 44 | + | |
| 45 | +@function font-weight($name) { | |
| 46 | + @return _token("font-weight", "weight-#{$name}", tokens.$font, "font-weight-#{$name}"); | |
| 47 | +} | |
| 48 | + | |
| 49 | +@function border($name) { | |
| 50 | + @return _token("border", "border-#{$name}", tokens.$size, "border-#{$name}"); | |
| 51 | +} | |
| 52 | + | |
| 53 | +@function padding($name) { | |
| 54 | + @return _token("padding", "padding-#{$name}", tokens.$size, "padding-#{$name}"); | |
| 55 | +} | |
| 56 | + | |
| 57 | +@function gap($name) { | |
| 58 | + @return _token("gap", "gap-#{$name}", tokens.$size, "gap-#{$name}"); | |
| 59 | +} | |
| 60 | + | |
| 61 | +@function radius($name) { | |
| 62 | + @return _token("radius", "radius-#{$name}", tokens.$size, "radius-#{$name}"); | |
| 63 | +} | |
| 64 | + | |
| 65 | +@function icon($name) { | |
| 66 | + @return _token("icon", "icon-#{$name}", tokens.$size, "icon-#{$name}"); | |
| 67 | +} | |
| 68 | + | |
| 69 | +@function shadow($name) { | |
| 70 | + @return _token("shadow", "shadow-#{$name}", tokens.$size, "shadow-#{$name}"); | |
| 71 | +} | |
| 72 | + | |
| 73 | +@function backdrop($name) { | |
| 74 | + @return _token("backdrop", "backdrop-#{$name}", tokens.$size, "backdrop-#{$name}"); | |
| 75 | +} | |
| 76 | + | |
| 77 | +@function font-size($name) { | |
| 78 | + @return _token("font-size", "font-size-#{$name}", tokens.$responsive, "font-size-#{$name}"); | |
| 79 | +} | |
| 80 | + | |
| 81 | +@function form($name) { | |
| 82 | + @return _token("form", "form-#{$name}", tokens.$responsive, "form-#{$name}"); | |
| 83 | +} | |
| 84 | + | |
| 85 | +@function grid($name) { | |
| 86 | + @return _token("grid", "grid-#{$name}", tokens.$responsive, "grid-#{$name}"); | |
| 87 | +} | |
| 88 | + | |
| 89 | +@function card($name) { | |
| 90 | + @return _token("card", "card-#{$name}", tokens.$responsive, "card-#{$name}"); | |
| 91 | +} | |
| 92 | + | |
| 93 | +@function modal($name) { | |
| 94 | + @return _token("modal", "modal-#{$name}", tokens.$responsive, "modal-#{$name}"); | |
| 95 | +} | |
| 96 | + | |
| 97 | +@function spacing($name) { | |
| 98 | + @return _token("spacing", "spacing-#{$name}", tokens.$responsive, "spacing-#{$name}"); | |
| 99 | +} | |
| 100 | + | |
| 101 | +@function section($name) { | |
| 102 | + @return _token("section", "section-#{$name}", tokens.$responsive, "section-#{$name}"); | |
| 103 | +} |
+++ @fox/styles/_mixins.scss
... | ... | @@ -0,0 +1,20 @@ |
| 1 | +// 믹스인 — 여러 선언이 항상 함께 가야 하는 패턴을 묶는다. | |
| 2 | + | |
| 3 | +@use "tokens"; | |
| 4 | + | |
| 5 | +/// PC 이상(>= 768px)에서 적용. 모바일 우선이므로 기본 스타일은 밖에 쓴다. | |
| 6 | +/// | |
| 7 | +/// ⚠️ 미디어 쿼리 조건부는 `var()`를 해석하지 못하므로 브레이크포인트만은 CSS 변수가 | |
| 8 | +/// 아닌 컴파일타임 값이다. 그래서 이 믹스인 경유가 강제된다. | |
| 9 | +@mixin pc { | |
| 10 | + @media (min-width: tokens.$breakpoint-pc) { | |
| 11 | + @content; | |
| 12 | + } | |
| 13 | +} | |
| 14 | + | |
| 15 | +/// PC 미만에서만 적용. `pc`와 경계가 겹치지 않도록 0.02px을 뺀다. | |
| 16 | +@mixin mobile { | |
| 17 | + @media (max-width: tokens.$breakpoint-pc - 0.02px) { | |
| 18 | + @content; | |
| 19 | + } | |
| 20 | +} |
+++ @fox/styles/_root.scss
... | ... | @@ -0,0 +1,71 @@ |
| 1 | +// 토큰 CSS 출력 — `tokens/`의 SCSS map을 `--fox-*` 커스텀 프로퍼티로 선언한다. | |
| 2 | +// | |
| 3 | +// **앱 전체에서 딱 한 번만 로드되어야 한다** (`index.scss` 경유). | |
| 4 | +// 컴포넌트의 `.module.scss`에서는 절대 `@use` 하지 않는다. | |
| 5 | +// | |
| 6 | +// map을 순회해 생성하므로 토큰을 추가할 때 이 파일은 손대지 않는다. | |
| 7 | + | |
| 8 | +@use "sass:map"; | |
| 9 | +@use "tokens"; | |
| 10 | + | |
| 11 | +:root { | |
| 12 | + // 기본은 OS 설정 추종. 아래 `[data-theme]` 규칙이 이 값만 덮어써 수동 선택을 | |
| 13 | + // 처리하므로, 색상 값 자체는 `tokens/_color.scss` 한 곳에만 존재한다. | |
| 14 | + color-scheme: light dark; | |
| 15 | + | |
| 16 | + @each $name, $value in tokens.$primitive { | |
| 17 | + --fox-primitive-#{$name}: #{$value}; | |
| 18 | + } | |
| 19 | + | |
| 20 | + @each $name, $value in tokens.$font { | |
| 21 | + --fox-font-#{$name}: #{$value}; | |
| 22 | + } | |
| 23 | + | |
| 24 | + @each $name, $value in tokens.$number { | |
| 25 | + --fox-number-#{$name}: #{$value}; | |
| 26 | + } | |
| 27 | + | |
| 28 | + @each $name, $value in tokens.$theme { | |
| 29 | + --fox-theme-#{$name}: #{$value}; | |
| 30 | + } | |
| 31 | + | |
| 32 | + // size / color / responsive 는 이름에 그룹이 이미 들어 있어 접두사가 `--fox-` 뿐이다. | |
| 33 | + @each $name, $value in tokens.$size { | |
| 34 | + --fox-#{$name}: #{$value}; | |
| 35 | + } | |
| 36 | + | |
| 37 | + @each $name, $value in tokens.$color { | |
| 38 | + --fox-color-#{$name}: #{$value}; | |
| 39 | + } | |
| 40 | + | |
| 41 | + // 모바일 우선 기본값. | |
| 42 | + @each $name, $value in tokens.$responsive { | |
| 43 | + --fox-#{$name}: #{$value}; | |
| 44 | + } | |
| 45 | +} | |
| 46 | + | |
| 47 | +// PC 모드 — 반응형 토큰만 덮어쓴다. 색상은 뷰포트와 무관하므로 여기 오지 않는다. | |
| 48 | +@media (min-width: tokens.$breakpoint-pc) { | |
| 49 | + :root { | |
| 50 | + @each $name, $value in tokens.$responsive-pc { | |
| 51 | + --fox-#{$name}: #{$value}; | |
| 52 | + } | |
| 53 | + } | |
| 54 | +} | |
| 55 | + | |
| 56 | +// 수동 테마 선택 — `color-scheme`만 고정하면 `light-dark()`가 해당 분기로 확정된다. | |
| 57 | +// 속성이 없거나 `"system"`이면 위 `:root`의 `light dark`가 유지되어 OS를 따른다. | |
| 58 | +:root[data-theme="light"] { | |
| 59 | + color-scheme: light; | |
| 60 | +} | |
| 61 | + | |
| 62 | +:root[data-theme="dark"] { | |
| 63 | + color-scheme: dark; | |
| 64 | +} | |
| 65 | + | |
| 66 | +// ⚠️ 1rem = 10px 규약의 근거. 브라우저 기본 글자 크기(16px)의 62.5%가 10px다. | |
| 67 | +// 모든 토큰의 rem 값이 이 선언을 전제로 계산돼 있다. | |
| 68 | +// 미디어 쿼리 안의 rem은 이 값의 영향을 받지 않는다(항상 브라우저 기본 크기 기준). | |
| 69 | +html { | |
| 70 | + font-size: 62.5%; | |
| 71 | +} |
+++ @fox/styles/abstracts.scss
... | ... | @@ -0,0 +1,17 @@ |
| 1 | +// 저작(authoring) 진입점 — 컴포넌트의 `.module.scss`가 `@use`하는 파일. | |
| 2 | +// | |
| 3 | +// @use "@fox/styles/abstracts" as fox; | |
| 4 | +// | |
| 5 | +// .button { | |
| 6 | +// padding: fox.padding(5) fox.padding(7); | |
| 7 | +// background: fox.color(button-primary-surface); | |
| 8 | +// border-radius: fox.radius(3); | |
| 9 | +// font-size: fox.font-size(label-md); | |
| 10 | +// } | |
| 11 | +// | |
| 12 | +// ⚠️ **이 파일은 CSS를 한 줄도 출력하지 않는다.** 함수·믹스인·map 정의만 있다. | |
| 13 | +// 실제 CSS 출력(토큰 선언)은 앱이 딱 한 번 import하는 `index.scss`가 전담한다. | |
| 14 | + | |
| 15 | +@forward "tokens"; | |
| 16 | +@forward "functions"; | |
| 17 | +@forward "mixins"; |
--- @fox/styles/index.scss
+++ @fox/styles/index.scss
... | ... | @@ -1,0 +1,10 @@ |
| 1 |
+// 글로벌 스타일 진입점 — **호스트 앱이 딱 한 번 import한다.** |
|
| 2 |
+// |
|
| 3 |
+// // app/globals.scss |
|
| 4 |
+// @use "@fox/styles"; |
|
| 5 |
+// |
|
| 6 |
+// 여기서만 CSS가 출력된다. 컴포넌트의 `.module.scss`는 이 파일이 아니라 |
|
| 7 |
+// `abstracts.scss`를 `@use`해야 한다 — 그쪽은 CSS 출력이 0이라 몇 번을 참조해도 |
|
| 8 |
+// 중복이 생기지 않는다. |
|
| 9 |
+ |
|
| 10 |
+@use "root"; |
+++ @fox/styles/tokens/_color.scss
... | ... | @@ -0,0 +1,283 @@ |
| 1 | +// ⚠️ 이 파일은 Figma Design Tokens export에서 **자동 생성**되었습니다. | |
| 2 | +// 원본: light.tokens.json / dark.tokens.json | |
| 3 | +// 손으로 고치지 말고 원본 JSON을 다시 export한 뒤 변환기를 재실행하세요. | |
| 4 | + | |
| 5 | +// 시맨틱 색상 — 라이트/다크 값을 `light-dark()` 한 줄에 함께 보유한다. | |
| 6 | +// 전환은 `_root.scss`의 `color-scheme`이 담당하므로 다크 전용 블록이 없고, | |
| 7 | +// 따라서 한쪽만 고쳐서 생기는 테마 불일치가 구조적으로 불가능하다. | |
| 8 | +// | |
| 9 | +// 값이 `var(--fox-primitive-*)` / `var(--fox-theme-*)`인 항목은 Figma의 alias를 | |
| 10 | +// 그대로 옮긴 것이다 — 바닥층을 바꾸면 여기까지 전파된다. | |
| 11 | +$color: ( | |
| 12 | + background-default: light-dark(var(--fox-primitive-neutral-0), var(--fox-primitive-neutral-100)), | |
| 13 | + background-neutral: light-dark(var(--fox-primitive-neutral-5), var(--fox-primitive-neutral-95)), | |
| 14 | + background-neutral-strong: light-dark(var(--fox-primitive-neutral-10), var(--fox-primitive-neutral-90)), | |
| 15 | + background-primary: light-dark(var(--fox-theme-primary-5), var(--fox-theme-primary-95)), | |
| 16 | + background-secondary: light-dark(var(--fox-theme-secondary-5), var(--fox-theme-secondary-95)), | |
| 17 | + background-accent: light-dark(var(--fox-theme-accent-5), var(--fox-theme-accent-95)), | |
| 18 | + background-dim: light-dark(var(--fox-primitive-alpha-black50), var(--fox-primitive-alpha-black75)), | |
| 19 | + background-dim-inverse: light-dark(var(--fox-primitive-alpha-white50), var(--fox-primitive-alpha-black75)), | |
| 20 | + surface-neutral-default: light-dark(var(--fox-primitive-neutral-0), var(--fox-primitive-neutral-95)), | |
| 21 | + surface-neutral-default-strong: light-dark(var(--fox-primitive-neutral-0), var(--fox-primitive-neutral-90)), | |
| 22 | + surface-neutral-default-static: light-dark(var(--fox-primitive-neutral-0), var(--fox-primitive-neutral-0)), | |
| 23 | + surface-neutral-default-inverse: light-dark(var(--fox-primitive-neutral-90), var(--fox-primitive-neutral-10)), | |
| 24 | + surface-neutral-default-inverse-static: light-dark(var(--fox-primitive-neutral-90), var(--fox-primitive-neutral-90)), | |
| 25 | + surface-neutral-gray: light-dark(var(--fox-primitive-neutral-5), var(--fox-primitive-neutral-90)), | |
| 26 | + surface-neutral-gray-strong: light-dark(var(--fox-primitive-neutral-10), var(--fox-primitive-neutral-80)), | |
| 27 | + surface-neutral-gray-stronger: light-dark(var(--fox-primitive-neutral-20), var(--fox-primitive-neutral-70)), | |
| 28 | + surface-neutral-gray-inverse: light-dark(var(--fox-primitive-neutral-80), var(--fox-primitive-neutral-20)), | |
| 29 | + surface-neutral-gray-strong-inverse: light-dark(var(--fox-primitive-neutral-70), var(--fox-primitive-neutral-30)), | |
| 30 | + surface-neutral-gray-stronger-inverse: light-dark(var(--fox-primitive-neutral-60), var(--fox-primitive-neutral-40)), | |
| 31 | + surface-neutral-disabled: light-dark(var(--fox-primitive-neutral-20), var(--fox-primitive-neutral-70)), | |
| 32 | + surface-theme-primary: light-dark(var(--fox-theme-primary-5), var(--fox-theme-primary-95)), | |
| 33 | + surface-theme-primary-inverse: light-dark(var(--fox-theme-primary-60), var(--fox-theme-primary-40)), | |
| 34 | + surface-theme-secondary: light-dark(var(--fox-theme-secondary-5), var(--fox-theme-secondary-95)), | |
| 35 | + surface-theme-secondary-inverse: light-dark(var(--fox-theme-secondary-60), var(--fox-theme-secondary-40)), | |
| 36 | + surface-theme-accent: light-dark(var(--fox-theme-accent-5), var(--fox-theme-accent-95)), | |
| 37 | + surface-theme-accent-inverse: light-dark(var(--fox-theme-accent-60), var(--fox-theme-accent-40)), | |
| 38 | + surface-system-danger: light-dark(var(--fox-primitive-danger-5), var(--fox-primitive-danger-95)), | |
| 39 | + surface-system-danger-inverse: light-dark(var(--fox-primitive-danger-60), var(--fox-primitive-danger-40)), | |
| 40 | + surface-system-warning: light-dark(var(--fox-primitive-warning-5), var(--fox-primitive-warning-95)), | |
| 41 | + surface-system-warning-inverse: light-dark(var(--fox-primitive-warning-60), var(--fox-primitive-warning-40)), | |
| 42 | + surface-system-success: light-dark(var(--fox-primitive-success-5), var(--fox-primitive-success-95)), | |
| 43 | + surface-system-success-inverse: light-dark(var(--fox-primitive-success-60), var(--fox-primitive-success-40)), | |
| 44 | + surface-system-information: light-dark(var(--fox-primitive-information-5), var(--fox-primitive-information-95)), | |
| 45 | + surface-system-information-inverse: light-dark(var(--fox-primitive-information-60), var(--fox-primitive-information-40)), | |
| 46 | + border-neutral-default: light-dark(var(--fox-primitive-neutral-30), var(--fox-primitive-neutral-70)), | |
| 47 | + border-neutral-subtle: light-dark(var(--fox-primitive-neutral-20), var(--fox-primitive-neutral-80)), | |
| 48 | + border-neutral-subtler: light-dark(var(--fox-primitive-neutral-10), var(--fox-primitive-neutral-90)), | |
| 49 | + border-neutral-strong: light-dark(var(--fox-primitive-neutral-90), var(--fox-primitive-neutral-0)), | |
| 50 | + border-neutral-disabled: light-dark(var(--fox-primitive-neutral-30), var(--fox-primitive-neutral-70)), | |
| 51 | + border-neutral-overlay: light-dark(var(--fox-primitive-alpha-black10), var(--fox-primitive-alpha-white25)), | |
| 52 | + border-theme-primary: light-dark(var(--fox-theme-primary-50), var(--fox-theme-primary-50)), | |
| 53 | + border-theme-primary-inverse: light-dark(var(--fox-theme-primary-10), var(--fox-theme-primary-90)), | |
| 54 | + border-theme-secondary: light-dark(var(--fox-theme-secondary-60), var(--fox-theme-secondary-40)), | |
| 55 | + border-theme-secondary-inverse: light-dark(var(--fox-theme-secondary-10), var(--fox-theme-secondary-90)), | |
| 56 | + border-theme-accent: light-dark(var(--fox-theme-accent-50), var(--fox-theme-accent-50)), | |
| 57 | + border-theme-accent-inverse: light-dark(var(--fox-theme-accent-10), var(--fox-theme-accent-90)), | |
| 58 | + border-system-danger: light-dark(var(--fox-primitive-danger-50), var(--fox-primitive-danger-50)), | |
| 59 | + border-system-danger-inverse: light-dark(var(--fox-primitive-danger-10), var(--fox-primitive-danger-90)), | |
| 60 | + border-system-warning: light-dark(var(--fox-primitive-warning-50), var(--fox-primitive-warning-50)), | |
| 61 | + border-system-warning-inverse: light-dark(var(--fox-primitive-warning-10), var(--fox-primitive-warning-90)), | |
| 62 | + border-system-success: light-dark(var(--fox-primitive-success-50), var(--fox-primitive-success-50)), | |
| 63 | + border-system-success-inverse: light-dark(var(--fox-primitive-success-10), var(--fox-primitive-success-90)), | |
| 64 | + border-system-information: light-dark(var(--fox-primitive-information-50), var(--fox-primitive-information-50)), | |
| 65 | + border-system-information-inverse: light-dark(var(--fox-primitive-information-10), var(--fox-primitive-information-90)), | |
| 66 | + font-neutral-strong: light-dark(var(--fox-primitive-neutral-90), var(--fox-primitive-neutral-5)), | |
| 67 | + font-neutral-default: light-dark(var(--fox-primitive-neutral-70), var(--fox-primitive-neutral-20)), | |
| 68 | + font-neutral-subtle: light-dark(var(--fox-primitive-neutral-50), var(--fox-primitive-neutral-30)), | |
| 69 | + font-neutral-subtler: light-dark(var(--fox-primitive-neutral-40), var(--fox-primitive-neutral-40)), | |
| 70 | + font-neutral-strong-inverse: light-dark(var(--fox-primitive-alpha-white100), var(--fox-primitive-neutral-95)), | |
| 71 | + font-neutral-default-inverse: light-dark(var(--fox-primitive-alpha-white75), var(--fox-primitive-neutral-90)), | |
| 72 | + font-neutral-subtle-inverse: light-dark(var(--fox-primitive-alpha-white50), var(--fox-primitive-neutral-80)), | |
| 73 | + font-neutral-subtler-inverse: light-dark(var(--fox-primitive-alpha-white25), var(--fox-primitive-neutral-70)), | |
| 74 | + font-neutral-static: light-dark(var(--fox-primitive-neutral-90), var(--fox-primitive-neutral-90)), | |
| 75 | + font-neutral-static-inverse: light-dark(var(--fox-primitive-neutral-0), var(--fox-primitive-neutral-0)), | |
| 76 | + font-neutral-disabled: light-dark(var(--fox-primitive-neutral-40), var(--fox-primitive-neutral-60)), | |
| 77 | + font-neutral-disabled-strong: light-dark(var(--fox-primitive-neutral-50), var(--fox-primitive-neutral-50)), | |
| 78 | + font-theme-primary: light-dark(var(--fox-theme-primary-60), var(--fox-theme-primary-20)), | |
| 79 | + font-theme-primary-inverse: light-dark(var(--fox-theme-primary-10), var(--fox-theme-primary-70)), | |
| 80 | + font-theme-secondary: light-dark(var(--fox-theme-secondary-60), var(--fox-theme-secondary-20)), | |
| 81 | + font-theme-secondary-inverse: light-dark(var(--fox-theme-secondary-10), var(--fox-theme-secondary-70)), | |
| 82 | + font-theme-accent: light-dark(var(--fox-theme-accent-60), var(--fox-theme-accent-20)), | |
| 83 | + font-theme-accent-inverse: light-dark(var(--fox-theme-accent-10), var(--fox-theme-accent-70)), | |
| 84 | + font-system-danger: light-dark(var(--fox-primitive-danger-60), var(--fox-primitive-danger-20)), | |
| 85 | + font-system-danger-inverse: light-dark(var(--fox-primitive-danger-10), var(--fox-primitive-danger-70)), | |
| 86 | + font-system-warning: light-dark(var(--fox-primitive-warning-60), var(--fox-primitive-warning-20)), | |
| 87 | + font-system-warning-inverse: light-dark(var(--fox-primitive-warning-10), var(--fox-primitive-warning-70)), | |
| 88 | + font-system-success: light-dark(var(--fox-primitive-success-60), var(--fox-primitive-success-20)), | |
| 89 | + font-system-success-inverse: light-dark(var(--fox-primitive-success-10), var(--fox-primitive-success-70)), | |
| 90 | + font-system-information: light-dark(var(--fox-primitive-information-60), var(--fox-primitive-information-20)), | |
| 91 | + font-system-information-inverse: light-dark(var(--fox-primitive-information-10), var(--fox-primitive-information-70)), | |
| 92 | + icon-neutral-strong: light-dark(var(--fox-primitive-neutral-90), var(--fox-primitive-neutral-5)), | |
| 93 | + icon-neutral-default: light-dark(var(--fox-primitive-neutral-70), var(--fox-primitive-neutral-20)), | |
| 94 | + icon-neutral-subtle: light-dark(var(--fox-primitive-neutral-50), var(--fox-primitive-neutral-30)), | |
| 95 | + icon-neutral-subtler: light-dark(var(--fox-primitive-neutral-40), var(--fox-primitive-neutral-40)), | |
| 96 | + icon-neutral-fill: light-dark(var(--fox-primitive-alpha-black25), var(--fox-primitive-alpha-white25)), | |
| 97 | + icon-neutral-strong-inverse: light-dark(var(--fox-primitive-alpha-white100), var(--fox-primitive-neutral-95)), | |
| 98 | + icon-neutral-default-inverse: light-dark(var(--fox-primitive-alpha-white75), var(--fox-primitive-neutral-90)), | |
| 99 | + icon-neutral-subtle-inverse: light-dark(var(--fox-primitive-alpha-white50), var(--fox-primitive-neutral-80)), | |
| 100 | + icon-neutral-subtler-inverse: light-dark(var(--fox-primitive-alpha-white25), var(--fox-primitive-neutral-70)), | |
| 101 | + icon-neutral-fill-inverse: light-dark(var(--fox-primitive-alpha-white25), var(--fox-primitive-neutral-70)), | |
| 102 | + icon-neutral-static: light-dark(var(--fox-primitive-neutral-90), var(--fox-primitive-neutral-90)), | |
| 103 | + icon-neutral-static-inverse: light-dark(var(--fox-primitive-neutral-0), var(--fox-primitive-neutral-0)), | |
| 104 | + icon-neutral-disabled: light-dark(var(--fox-primitive-neutral-40), var(--fox-primitive-neutral-60)), | |
| 105 | + icon-neutral-disabled-strong: light-dark(var(--fox-primitive-neutral-50), var(--fox-primitive-neutral-50)), | |
| 106 | + icon-theme-primary: light-dark(var(--fox-theme-primary-50), var(--fox-theme-primary-20)), | |
| 107 | + icon-theme-primary-strong: light-dark(var(--fox-theme-primary-60), var(--fox-theme-primary-30)), | |
| 108 | + icon-theme-primary-static: light-dark(var(--fox-theme-primary-50), var(--fox-theme-primary-50)), | |
| 109 | + icon-theme-primary-inverse: light-dark(var(--fox-theme-primary-10), var(--fox-theme-primary-70)), | |
| 110 | + icon-theme-secondary: light-dark(var(--fox-theme-secondary-60), var(--fox-theme-secondary-30)), | |
| 111 | + icon-theme-secondary-strong: light-dark(var(--fox-theme-secondary-70), var(--fox-theme-secondary-40)), | |
| 112 | + icon-theme-secondary-static: light-dark(var(--fox-theme-secondary-50), var(--fox-theme-secondary-50)), | |
| 113 | + icon-theme-secondary-inverse: light-dark(var(--fox-theme-secondary-10), var(--fox-theme-secondary-70)), | |
| 114 | + icon-theme-accent: light-dark(var(--fox-theme-accent-50), var(--fox-theme-accent-20)), | |
| 115 | + icon-theme-accent-strong: light-dark(var(--fox-theme-accent-60), var(--fox-theme-accent-30)), | |
| 116 | + icon-theme-accent-static: light-dark(var(--fox-theme-accent-50), var(--fox-theme-accent-50)), | |
| 117 | + icon-theme-accent-inverse: light-dark(var(--fox-theme-accent-10), var(--fox-theme-accent-70)), | |
| 118 | + icon-system-danger: light-dark(var(--fox-primitive-danger-50), var(--fox-primitive-danger-20)), | |
| 119 | + icon-system-danger-strong: light-dark(var(--fox-primitive-danger-60), var(--fox-primitive-danger-30)), | |
| 120 | + icon-system-danger-inverse: light-dark(var(--fox-primitive-danger-10), var(--fox-primitive-danger-70)), | |
| 121 | + icon-system-warning: light-dark(var(--fox-primitive-warning-50), var(--fox-primitive-warning-20)), | |
| 122 | + icon-system-warning-strong: light-dark(var(--fox-primitive-warning-60), var(--fox-primitive-warning-30)), | |
| 123 | + icon-system-warning-inverse: light-dark(var(--fox-primitive-warning-10), var(--fox-primitive-warning-70)), | |
| 124 | + icon-system-success: light-dark(var(--fox-primitive-success-50), var(--fox-primitive-success-20)), | |
| 125 | + icon-system-success-strong: light-dark(var(--fox-primitive-success-60), var(--fox-primitive-success-30)), | |
| 126 | + icon-system-success-inverse: light-dark(var(--fox-primitive-success-10), var(--fox-primitive-success-70)), | |
| 127 | + icon-system-information: light-dark(var(--fox-primitive-information-50), var(--fox-primitive-information-20)), | |
| 128 | + icon-system-information-strong: light-dark(var(--fox-primitive-information-60), var(--fox-primitive-information-30)), | |
| 129 | + icon-system-information-inverse: light-dark(var(--fox-primitive-information-10), var(--fox-primitive-information-70)), | |
| 130 | + element-neutral-strong: light-dark(var(--fox-primitive-neutral-60), var(--fox-primitive-neutral-40)), | |
| 131 | + element-neutral-default: light-dark(var(--fox-primitive-neutral-50), var(--fox-primitive-neutral-50)), | |
| 132 | + element-neutral-subtle: light-dark(var(--fox-primitive-neutral-10), var(--fox-primitive-neutral-90)), | |
| 133 | + element-neutral-subtler: light-dark(var(--fox-primitive-neutral-5), var(--fox-primitive-neutral-95)), | |
| 134 | + element-neutral-inverse: light-dark(var(--fox-primitive-neutral-0), var(--fox-primitive-neutral-95)), | |
| 135 | + element-neutral-inverse-static: light-dark(var(--fox-primitive-neutral-0), var(--fox-primitive-neutral-0)), | |
| 136 | + element-neutral-alpha: light-dark(rgb(0 0 0 / 0.25), rgb(0 0 0 / 0.5)), | |
| 137 | + element-neutral-alpha-inverse: light-dark(rgb(255 255 255 / 0.5), rgb(255 255 255 / 0.25)), | |
| 138 | + element-neutral-disabled: light-dark(var(--fox-primitive-neutral-20), var(--fox-primitive-neutral-80)), | |
| 139 | + element-neutral-disabled-strong: light-dark(var(--fox-primitive-neutral-40), var(--fox-primitive-neutral-60)), | |
| 140 | + element-theme-primary: light-dark(var(--fox-theme-primary-50), var(--fox-theme-primary-50)), | |
| 141 | + element-theme-primary-strong: light-dark(var(--fox-theme-primary-60), var(--fox-theme-primary-40)), | |
| 142 | + element-theme-primary-subtle: light-dark(var(--fox-theme-primary-10), var(--fox-theme-primary-90)), | |
| 143 | + element-theme-primary-subtler: light-dark(var(--fox-theme-primary-5), var(--fox-theme-primary-95)), | |
| 144 | + element-theme-secondary: light-dark(var(--fox-theme-secondary-70), var(--fox-theme-secondary-60)), | |
| 145 | + element-theme-secondary-strong: light-dark(var(--fox-theme-secondary-70), var(--fox-theme-secondary-60)), | |
| 146 | + element-theme-secondary-subtle: light-dark(var(--fox-theme-secondary-10), var(--fox-theme-secondary-90)), | |
| 147 | + element-theme-secondary-subtler: light-dark(var(--fox-theme-secondary-5), var(--fox-theme-secondary-95)), | |
| 148 | + element-theme-accent: light-dark(var(--fox-theme-accent-50), var(--fox-theme-accent-50)), | |
| 149 | + element-theme-accent-strong: light-dark(var(--fox-theme-accent-60), var(--fox-theme-accent-40)), | |
| 150 | + element-theme-accent-subtle: light-dark(var(--fox-theme-accent-10), var(--fox-theme-accent-90)), | |
| 151 | + element-theme-accent-subtler: light-dark(var(--fox-theme-accent-5), var(--fox-theme-accent-95)), | |
| 152 | + element-system-danger: light-dark(var(--fox-primitive-danger-50), var(--fox-primitive-danger-50)), | |
| 153 | + element-system-danger-strong: light-dark(var(--fox-primitive-danger-60), var(--fox-primitive-danger-40)), | |
| 154 | + element-system-danger-inverse: light-dark(var(--fox-primitive-danger-5), var(--fox-primitive-danger-95)), | |
| 155 | + element-system-warning: light-dark(var(--fox-primitive-warning-30), var(--fox-primitive-warning-30)), | |
| 156 | + element-system-warning-strong: light-dark(var(--fox-primitive-warning-50), var(--fox-primitive-warning-20)), | |
| 157 | + element-system-warning-inverse: light-dark(var(--fox-primitive-warning-5), var(--fox-primitive-warning-95)), | |
| 158 | + element-system-success: light-dark(var(--fox-primitive-success-50), var(--fox-primitive-success-50)), | |
| 159 | + element-system-success-strong: light-dark(var(--fox-primitive-success-60), var(--fox-primitive-success-40)), | |
| 160 | + element-system-success-inverse: light-dark(var(--fox-primitive-success-5), var(--fox-primitive-success-95)), | |
| 161 | + element-system-information: light-dark(var(--fox-primitive-information-50), var(--fox-primitive-information-50)), | |
| 162 | + element-system-information-strong: light-dark(var(--fox-primitive-information-60), var(--fox-primitive-information-40)), | |
| 163 | + element-system-information-inverse: light-dark(var(--fox-primitive-information-5), var(--fox-primitive-information-95)), | |
| 164 | + alpha-default100: light-dark(var(--fox-primitive-alpha-white100), var(--fox-primitive-alpha-black100)), | |
| 165 | + alpha-default75: light-dark(var(--fox-primitive-alpha-white75), var(--fox-primitive-alpha-black75)), | |
| 166 | + alpha-default50: light-dark(var(--fox-primitive-alpha-white50), var(--fox-primitive-alpha-black50)), | |
| 167 | + alpha-default25: light-dark(var(--fox-primitive-alpha-white25), var(--fox-primitive-alpha-black25)), | |
| 168 | + alpha-default10: light-dark(var(--fox-primitive-alpha-white10), var(--fox-primitive-alpha-black10)), | |
| 169 | + alpha-default0: light-dark(var(--fox-primitive-alpha-white0), var(--fox-primitive-alpha-black0)), | |
| 170 | + alpha-default-inverse100: light-dark(var(--fox-primitive-alpha-black100), var(--fox-primitive-alpha-white100)), | |
| 171 | + alpha-default-inverse75: light-dark(var(--fox-primitive-alpha-black75), var(--fox-primitive-alpha-white75)), | |
| 172 | + alpha-default-inverse50: light-dark(var(--fox-primitive-alpha-black50), var(--fox-primitive-alpha-white50)), | |
| 173 | + alpha-default-inverse25: light-dark(var(--fox-primitive-alpha-black25), var(--fox-primitive-alpha-white25)), | |
| 174 | + alpha-default-inverse10: light-dark(var(--fox-primitive-alpha-black10), var(--fox-primitive-alpha-white10)), | |
| 175 | + alpha-default-inverse0: light-dark(var(--fox-primitive-alpha-black0), var(--fox-primitive-alpha-white0)), | |
| 176 | + action-default: light-dark(var(--fox-primitive-neutral-0), var(--fox-primitive-neutral-100)), | |
| 177 | + action-default-hover: light-dark(var(--fox-primitive-neutral-5), var(--fox-primitive-neutral-95)), | |
| 178 | + action-default-pressed: light-dark(var(--fox-primitive-neutral-10), var(--fox-primitive-neutral-90)), | |
| 179 | + action-default-selected: light-dark(var(--fox-primitive-neutral-10), var(--fox-primitive-neutral-90)), | |
| 180 | + action-default-active: light-dark(var(--fox-primitive-neutral-70), var(--fox-primitive-neutral-60)), | |
| 181 | + action-neutral: light-dark(var(--fox-primitive-neutral-5), var(--fox-primitive-neutral-95)), | |
| 182 | + action-neutral-hover: light-dark(var(--fox-primitive-neutral-10), var(--fox-primitive-neutral-90)), | |
| 183 | + action-neutral-pressed: light-dark(var(--fox-primitive-neutral-20), var(--fox-primitive-neutral-80)), | |
| 184 | + action-neutral-selected: light-dark(var(--fox-primitive-neutral-20), var(--fox-primitive-neutral-80)), | |
| 185 | + action-neutral-active: light-dark(var(--fox-primitive-neutral-70), var(--fox-primitive-neutral-60)), | |
| 186 | + action-primary: light-dark(var(--fox-primitive-alpha-white0), var(--fox-primitive-alpha-black0)), | |
| 187 | + action-primary-hover: light-dark(var(--fox-theme-primary-5), var(--fox-theme-primary-95)), | |
| 188 | + action-primary-pressed: light-dark(var(--fox-theme-primary-10), var(--fox-theme-primary-90)), | |
| 189 | + action-primary-selected: light-dark(var(--fox-theme-primary-5), var(--fox-theme-primary-95)), | |
| 190 | + action-primary-active: light-dark(var(--fox-theme-primary-50), var(--fox-theme-primary-50)), | |
| 191 | + action-secondary: light-dark(var(--fox-primitive-alpha-white0), var(--fox-primitive-alpha-black0)), | |
| 192 | + action-secondary-hover: light-dark(var(--fox-theme-secondary-5), var(--fox-theme-secondary-95)), | |
| 193 | + action-secondary-pressed: light-dark(var(--fox-theme-secondary-10), var(--fox-theme-secondary-90)), | |
| 194 | + action-secondary-selected: light-dark(var(--fox-theme-secondary-5), var(--fox-theme-secondary-90)), | |
| 195 | + action-secondary-active: light-dark(var(--fox-theme-secondary-70), var(--fox-theme-secondary-60)), | |
| 196 | + button-primary-surface: light-dark(var(--fox-theme-primary-50), var(--fox-theme-primary-50)), | |
| 197 | + button-primary-surface-hover: light-dark(var(--fox-theme-primary-60), var(--fox-theme-primary-60)), | |
| 198 | + button-primary-surface-pressed: light-dark(var(--fox-theme-primary-70), var(--fox-theme-primary-70)), | |
| 199 | + button-primary-border: light-dark(var(--fox-primitive-alpha-black0), var(--fox-primitive-alpha-black0)), | |
| 200 | + button-primary-font: light-dark(var(--fox-primitive-alpha-white100), var(--fox-primitive-alpha-white100)), | |
| 201 | + button-secondary-surface: light-dark(var(--fox-theme-primary-5), var(--fox-theme-primary-95)), | |
| 202 | + button-secondary-surface-hover: light-dark(var(--fox-theme-primary-10), var(--fox-theme-primary-90)), | |
| 203 | + button-secondary-surface-pressed: light-dark(var(--fox-theme-primary-20), var(--fox-theme-primary-80)), | |
| 204 | + button-secondary-border: light-dark(var(--fox-theme-primary-30), var(--fox-theme-primary-70)), | |
| 205 | + button-secondary-font: light-dark(var(--fox-theme-primary-60), var(--fox-theme-primary-30)), | |
| 206 | + button-default-surface: light-dark(var(--fox-primitive-alpha-white0), var(--fox-primitive-alpha-black0)), | |
| 207 | + button-default-surface-hover: light-dark(var(--fox-primitive-neutral-5), var(--fox-primitive-neutral-95)), | |
| 208 | + button-default-surface-pressed: light-dark(var(--fox-primitive-neutral-10), var(--fox-primitive-neutral-80)), | |
| 209 | + button-default-border: light-dark(var(--fox-primitive-neutral-30), var(--fox-primitive-neutral-70)), | |
| 210 | + button-default-font: light-dark(var(--fox-primitive-neutral-70), var(--fox-primitive-neutral-20)), | |
| 211 | + button-text-surface: light-dark(var(--fox-primitive-alpha-white0), var(--fox-primitive-alpha-black0)), | |
| 212 | + button-text-surface-hover: light-dark(var(--fox-theme-secondary-5), var(--fox-theme-secondary-95)), | |
| 213 | + button-text-surface-pressed: light-dark(var(--fox-theme-secondary-10), var(--fox-theme-secondary-90)), | |
| 214 | + button-text-visited: light-dark(#5917B8, #C49FF9), | |
| 215 | + button-disabled-surface: light-dark(var(--fox-primitive-neutral-20), var(--fox-primitive-neutral-80)), | |
| 216 | + button-disabled-border: light-dark(var(--fox-primitive-neutral-30), var(--fox-primitive-neutral-70)), | |
| 217 | + button-disabled-font: light-dark(var(--fox-primitive-neutral-50), var(--fox-primitive-neutral-50)), | |
| 218 | + input-surface: light-dark(var(--fox-primitive-neutral-0), var(--fox-primitive-neutral-90)), | |
| 219 | + input-surface-disabled: light-dark(var(--fox-primitive-neutral-20), var(--fox-primitive-neutral-95)), | |
| 220 | + input-border: light-dark(var(--fox-primitive-neutral-60), var(--fox-primitive-neutral-40)), | |
| 221 | + input-border-active: light-dark(var(--fox-theme-primary-50), var(--fox-theme-primary-50)), | |
| 222 | + input-border-error: light-dark(var(--fox-primitive-danger-50), var(--fox-primitive-danger-50)), | |
| 223 | + input-border-success: light-dark(var(--fox-primitive-success-50), var(--fox-primitive-success-50)), | |
| 224 | + input-border-disabled: light-dark(var(--fox-primitive-neutral-30), var(--fox-primitive-neutral-70)), | |
| 225 | + chart-default-bar: light-dark(var(--fox-theme-primary-20), var(--fox-theme-primary-70)), | |
| 226 | + chart-default-surface: light-dark(var(--fox-theme-primary-10), var(--fox-theme-primary-70)), | |
| 227 | + chart-default-line: light-dark(var(--fox-theme-primary-50), var(--fox-theme-primary-70)), | |
| 228 | + chart-default-disabled: light-dark(var(--fox-primitive-neutral-20), var(--fox-primitive-neutral-70)), | |
| 229 | + chart-level-1: light-dark(var(--fox-theme-primary-90), var(--fox-theme-primary-10)), | |
| 230 | + chart-level-2: light-dark(var(--fox-theme-primary-70), var(--fox-theme-primary-20)), | |
| 231 | + chart-level-3: light-dark(var(--fox-theme-primary-50), var(--fox-theme-primary-40)), | |
| 232 | + chart-level-4: light-dark(var(--fox-theme-primary-40), var(--fox-theme-primary-50)), | |
| 233 | + chart-level-5: light-dark(var(--fox-theme-primary-20), var(--fox-theme-primary-70)), | |
| 234 | + chart-level-6: light-dark(var(--fox-theme-primary-10), var(--fox-theme-primary-90)), | |
| 235 | + chart-level-7: light-dark(var(--fox-primitive-neutral-60), var(--fox-primitive-neutral-10)), | |
| 236 | + chart-level-8: light-dark(var(--fox-primitive-neutral-40), var(--fox-primitive-neutral-20)), | |
| 237 | + chart-level-9: light-dark(var(--fox-primitive-neutral-20), var(--fox-primitive-neutral-40)), | |
| 238 | + chart-level-10: light-dark(var(--fox-primitive-neutral-10), var(--fox-primitive-neutral-60)), | |
| 239 | + chart-type-1-bar: light-dark(var(--fox-theme-primary-20), var(--fox-theme-primary-50)), | |
| 240 | + chart-type-1-line: light-dark(var(--fox-theme-primary-40), var(--fox-theme-primary-30)), | |
| 241 | + chart-type-1-surface: light-dark(var(--fox-theme-primary-5), var(--fox-theme-primary-95)), | |
| 242 | + chart-type-2-bar: light-dark(var(--fox-theme-accent-20), var(--fox-theme-accent-50)), | |
| 243 | + chart-type-2-line: light-dark(var(--fox-theme-accent-30), var(--fox-theme-accent-30)), | |
| 244 | + chart-type-2-surface: light-dark(var(--fox-theme-accent-5), var(--fox-theme-accent-95)), | |
| 245 | + chart-type-3-bar: light-dark(var(--fox-theme-secondary-20), var(--fox-theme-secondary-50)), | |
| 246 | + chart-type-3-line: light-dark(var(--fox-theme-secondary-30), var(--fox-theme-secondary-30)), | |
| 247 | + chart-type-3-surface: light-dark(var(--fox-theme-secondary-5), var(--fox-theme-secondary-95)), | |
| 248 | + chart-type-4-bar: light-dark(#FFDF94, #DEB451), | |
| 249 | + chart-type-4-line: light-dark(#FBC35A, #FBC35A), | |
| 250 | + chart-type-4-surface: light-dark(#FFF7E9, #2F2D28), | |
| 251 | + chart-type-5-bar: light-dark(#FF9F85, #D96645), | |
| 252 | + chart-type-5-line: light-dark(#FF7C57, #FF7C57), | |
| 253 | + chart-type-5-surface: light-dark(#FFECE7, #3F3330), | |
| 254 | + chart-alt-level-1: light-dark(#002D31, #D4F4F7), | |
| 255 | + chart-alt-level-2: light-dark(#005A62, #9EE7EE), | |
| 256 | + chart-alt-level-3: light-dark(#00828E, #1EBCCB), | |
| 257 | + chart-alt-level-4: light-dark(#1EBCCB, #00828E), | |
| 258 | + chart-alt-level-5: light-dark(#9EE7EE, #005A62), | |
| 259 | + chart-alt-level-6: light-dark(#D4F4F7, #002D31), | |
| 260 | + chart-alt-level-7: light-dark(var(--fox-primitive-neutral-60), var(--fox-primitive-neutral-10)), | |
| 261 | + chart-alt-level-8: light-dark(var(--fox-primitive-neutral-40), var(--fox-primitive-neutral-20)), | |
| 262 | + chart-alt-level-9: light-dark(var(--fox-primitive-neutral-20), var(--fox-primitive-neutral-40)), | |
| 263 | + chart-alt-level-10: light-dark(var(--fox-primitive-neutral-10), var(--fox-primitive-neutral-60)), | |
| 264 | + chart-alt-default-bar: light-dark(#9EE7EE, #005A62), | |
| 265 | + chart-alt-default-surface: light-dark(#D4F4F7, #005A62), | |
| 266 | + chart-alt-default-line: light-dark(#00828E, #005A62), | |
| 267 | + chart-alt-default-disabled: light-dark(var(--fox-primitive-neutral-20), var(--fox-primitive-neutral-70)), | |
| 268 | + chart-alt-type-1-bar: light-dark(#9EE7EE, #00828E), | |
| 269 | + chart-alt-type-1-line: light-dark(#1EBCCB, #57D2DE), | |
| 270 | + chart-alt-type-1-surface: light-dark(#E9FAFC, #001A1C), | |
| 271 | + chart-alt-type-2-bar: light-dark(#CDE566, #5E8100), | |
| 272 | + chart-alt-type-2-line: light-dark(#A7CC00, #A7CC00), | |
| 273 | + chart-alt-type-2-surface: light-dark(#F2F9D9, #121900), | |
| 274 | + chart-alt-type-3-bar: light-dark(#97CBED, #00577D), | |
| 275 | + chart-alt-type-3-line: light-dark(#52A6D3, #52A6D3), | |
| 276 | + chart-alt-type-3-surface: light-dark(#E8F4FC, #00080C), | |
| 277 | + chart-alt-type-4-bar: light-dark(#FFDF94, #DEB451), | |
| 278 | + chart-alt-type-4-line: light-dark(#FBC35A, #FBC35A), | |
| 279 | + chart-alt-type-4-surface: light-dark(#FFF7E9, #2F2D28), | |
| 280 | + chart-alt-type-5-bar: light-dark(#FF9F85, #D96645), | |
| 281 | + chart-alt-type-5-line: light-dark(#FF7C57, #FF7C57), | |
| 282 | + chart-alt-type-5-surface: light-dark(#FFECE7, #3F3330) | |
| 283 | +); |
+++ @fox/styles/tokens/_index.scss
... | ... | @@ -0,0 +1,10 @@ |
| 1 | +// 토큰 map 모음 — **CSS를 한 줄도 출력하지 않는다.** | |
| 2 | +// 값 선언(이 폴더)과 CSS 출력(`../_root.scss`)을 분리해 두었기 때문에, 컴포넌트의 | |
| 3 | +// `.module.scss`가 몇 개든 `abstracts`를 `@use`해도 `:root` 블록이 중복 출력되지 않는다 | |
| 4 | +// (각 CSS Module은 별도 컴파일 단위라 CSS를 내보내는 모듈을 공유하면 파일마다 복제된다). | |
| 5 | + | |
| 6 | +@forward "primitive"; | |
| 7 | +@forward "theme"; | |
| 8 | +@forward "size"; | |
| 9 | +@forward "color"; | |
| 10 | +@forward "responsive"; |
+++ @fox/styles/tokens/_primitive.scss
... | ... | @@ -0,0 +1,112 @@ |
| 1 | +// ⚠️ 이 파일은 Figma Design Tokens export에서 **자동 생성**되었습니다. | |
| 2 | +// 원본: primitive.tokens.json | |
| 3 | +// 손으로 고치지 말고 원본 JSON을 다시 export한 뒤 변환기를 재실행하세요. | |
| 4 | + | |
| 5 | +// 원시 팔레트 — 화면에서 직접 쓰지 않는다. 시맨틱 토큰(`$color`)이 참조하는 바닥층이다. | |
| 6 | +$primitive: ( | |
| 7 | + neutral-0: #FFFFFF, | |
| 8 | + neutral-5: #F4F5F6, | |
| 9 | + neutral-10: #E6E8EA, | |
| 10 | + neutral-20: #CDD1D5, | |
| 11 | + neutral-30: #B1B8BE, | |
| 12 | + neutral-40: #8A949E, | |
| 13 | + neutral-50: #6D7882, | |
| 14 | + neutral-60: #58616A, | |
| 15 | + neutral-70: #464C53, | |
| 16 | + neutral-80: #33363D, | |
| 17 | + neutral-90: #1E2124, | |
| 18 | + neutral-95: #131416, | |
| 19 | + neutral-100: #000000, | |
| 20 | + danger-5: #FDEFEC, | |
| 21 | + danger-10: #FCDFD9, | |
| 22 | + danger-20: #F7AFA1, | |
| 23 | + danger-30: #F48771, | |
| 24 | + danger-40: #F05F42, | |
| 25 | + danger-50: #DE3412, | |
| 26 | + danger-60: #BD2C0F, | |
| 27 | + danger-70: #8A240F, | |
| 28 | + danger-80: #5C180A, | |
| 29 | + danger-90: #390D05, | |
| 30 | + danger-95: #260903, | |
| 31 | + warning-5: #FFF3DB, | |
| 32 | + warning-10: #FFE0A3, | |
| 33 | + warning-20: #FFC95C, | |
| 34 | + warning-30: #FFB114, | |
| 35 | + warning-40: #C78500, | |
| 36 | + warning-50: #9E6A00, | |
| 37 | + warning-60: #8A5C00, | |
| 38 | + warning-70: #614100, | |
| 39 | + warning-80: #422C00, | |
| 40 | + warning-90: #2E1F00, | |
| 41 | + warning-95: #241800, | |
| 42 | + success-5: #EAF6EC, | |
| 43 | + success-10: #D8EEDD, | |
| 44 | + success-20: #A9DAB4, | |
| 45 | + success-30: #7EC88E, | |
| 46 | + success-40: #3FA654, | |
| 47 | + success-50: #228738, | |
| 48 | + success-60: #267337, | |
| 49 | + success-70: #285D33, | |
| 50 | + success-80: #1F4727, | |
| 51 | + success-90: #122B18, | |
| 52 | + success-95: #0E2012, | |
| 53 | + information-5: #E7F4FE, | |
| 54 | + information-10: #D3EBFD, | |
| 55 | + information-20: #9ED2FA, | |
| 56 | + information-30: #5FB5F7, | |
| 57 | + information-40: #2098F3, | |
| 58 | + information-50: #0B78CB, | |
| 59 | + information-60: #096AB3, | |
| 60 | + information-70: #085691, | |
| 61 | + information-80: #053961, | |
| 62 | + information-90: #03253F, | |
| 63 | + information-95: #021A2C, | |
| 64 | + alpha-black100: #000000, | |
| 65 | + alpha-black75: rgb(0 0 0 / 0.75), | |
| 66 | + alpha-black50: rgb(0 0 0 / 0.5), | |
| 67 | + alpha-black25: rgb(0 0 0 / 0.25), | |
| 68 | + alpha-black10: rgb(0 0 0 / 0.1), | |
| 69 | + alpha-black0: rgb(0 0 0 / 0), | |
| 70 | + alpha-white100: #FFFFFF, | |
| 71 | + alpha-white75: rgb(255 255 255 / 0.75), | |
| 72 | + alpha-white50: rgb(255 255 255 / 0.5), | |
| 73 | + alpha-white25: rgb(255 255 255 / 0.25), | |
| 74 | + alpha-white10: rgb(255 255 255 / 0.1), | |
| 75 | + alpha-white0: rgb(255 255 255 / 0) | |
| 76 | +); | |
| 77 | + | |
| 78 | +// 서체 — family는 문자열, weight는 무단위 수치(길이가 아니므로 rem 변환 대상이 아니다). | |
| 79 | +$font: ( | |
| 80 | + family-title: Pretendard, | |
| 81 | + family-body: Pretendard, | |
| 82 | + weight-regular: 400, | |
| 83 | + weight-medium: 600, | |
| 84 | + weight-bold: 800 | |
| 85 | +); | |
| 86 | + | |
| 87 | +// 원시 수치 스케일 — size 컬렉션이 참조하는 값들. 1rem = 10px 기준으로 변환됨. | |
| 88 | +$number: ( | |
| 89 | + 0: 0, | |
| 90 | + 1: 0.1rem, | |
| 91 | + 2: 0.2rem, | |
| 92 | + 3: 0.4rem, | |
| 93 | + 4: 0.6rem, | |
| 94 | + 5: 0.8rem, | |
| 95 | + 6: 1rem, | |
| 96 | + 7: 1.2rem, | |
| 97 | + 8: 1.6rem, | |
| 98 | + 9: 2rem, | |
| 99 | + 10: 2.4rem, | |
| 100 | + 11: 2.8rem, | |
| 101 | + 12: 3.2rem, | |
| 102 | + 13: 3.6rem, | |
| 103 | + 14: 4rem, | |
| 104 | + 15: 4.4rem, | |
| 105 | + 16: 4.8rem, | |
| 106 | + 17: 5.6rem, | |
| 107 | + 18: 6.4rem, | |
| 108 | + 19: 7.2rem, | |
| 109 | + 20: 8rem, | |
| 110 | + 21: 9.6rem, | |
| 111 | + max: 100rem | |
| 112 | +); |
+++ @fox/styles/tokens/_responsive.scss
... | ... | @@ -0,0 +1,163 @@ |
| 1 | +// ⚠️ 이 파일은 Figma Design Tokens export에서 **자동 생성**되었습니다. | |
| 2 | +// 원본: mobile.tokens.json / pc.tokens.json | |
| 3 | +// 손으로 고치지 말고 원본 JSON을 다시 export한 뒤 변환기를 재실행하세요. | |
| 4 | + | |
| 5 | +// 뷰포트에 따라 달라지는 크기 토큰. 키는 두 모드가 완전히 동일하다. | |
| 6 | +// 모바일 우선 — `$responsive`가 기본값이고 `$responsive-pc`가 브레이크포인트 이상에서 덮는다. | |
| 7 | +$breakpoint-pc: 768px; | |
| 8 | + | |
| 9 | +$responsive: ( | |
| 10 | + font-size-display-lg: 4.4rem, | |
| 11 | + font-size-display-md: 3.2rem, | |
| 12 | + font-size-display-sm: 2.8rem, | |
| 13 | + font-size-heading-xlg: 2.8rem, | |
| 14 | + font-size-heading-lg: 2.4rem, | |
| 15 | + font-size-heading-md: 2rem, | |
| 16 | + font-size-heading-sm: 1.7rem, | |
| 17 | + font-size-heading-xsm: 1.5rem, | |
| 18 | + font-size-heading-2xsm: 1.3rem, | |
| 19 | + font-size-body-lg: 1.5rem, | |
| 20 | + font-size-body-md: 1.3rem, | |
| 21 | + font-size-body-sm: 1.2rem, | |
| 22 | + font-size-body-xsm: 1rem, | |
| 23 | + font-size-label-lg: 1.5rem, | |
| 24 | + font-size-label-md: 1.3rem, | |
| 25 | + font-size-label-sm: 1.2rem, | |
| 26 | + font-size-label-xsm: 1rem, | |
| 27 | + form-radius-xsm: 0.4rem, | |
| 28 | + form-radius-sm: 0.4rem, | |
| 29 | + form-radius-md: 0.8rem, | |
| 30 | + form-radius-lg: 1.2rem, | |
| 31 | + form-padding-lg: 2rem, | |
| 32 | + form-padding-md: 1.2rem, | |
| 33 | + form-padding-sm: 0.8rem, | |
| 34 | + form-padding-xsm: 0.4rem, | |
| 35 | + form-icon-lg: 2rem, | |
| 36 | + form-icon-md: 1.6rem, | |
| 37 | + form-icon-sm: 1.2rem, | |
| 38 | + form-icon-xsm: 1.2rem, | |
| 39 | + form-height-xsm: 2rem, | |
| 40 | + form-height-sm: 2.8rem, | |
| 41 | + form-height-md: 4rem, | |
| 42 | + form-height-lg: 4.8rem, | |
| 43 | + grid-wrap-full: 38rem, | |
| 44 | + grid-wrap-default: 34.8rem, | |
| 45 | + grid-wrap-sm: 32rem, | |
| 46 | + grid-wrap-xsm: 28rem, | |
| 47 | + grid-margin-full: 0, | |
| 48 | + grid-margin-default: 1.6rem, | |
| 49 | + grid-margin-sm: 3rem, | |
| 50 | + grid-margin-xsm: 5rem, | |
| 51 | + grid-gutter-sm: 0.8rem, | |
| 52 | + grid-gutter-md: 1.6rem, | |
| 53 | + grid-gutter-lg: 2rem, | |
| 54 | + grid-gutter-xlg: 2rem, | |
| 55 | + grid-gutter-2xlg: 4rem, | |
| 56 | + card-radius-xlg: 1.6rem, | |
| 57 | + card-radius-lg: 1.6rem, | |
| 58 | + card-radius-md: 1.2rem, | |
| 59 | + card-radius-sm: 0.8rem, | |
| 60 | + card-padding-xlg: 2.4rem, | |
| 61 | + card-padding-lg: 2rem, | |
| 62 | + card-padding-md: 1.6rem, | |
| 63 | + card-padding-sm: 1.2rem, | |
| 64 | + modal-width-full: 38rem, | |
| 65 | + modal-width-lg: 34.8rem, | |
| 66 | + modal-width-md: 34.8rem, | |
| 67 | + modal-width-sm: 34.8rem, | |
| 68 | + modal-padding-default: 2.4rem, | |
| 69 | + modal-btn-close: 5.6rem, | |
| 70 | + spacing-top-2xlg: 8rem, | |
| 71 | + spacing-top-xlg: 5.6rem, | |
| 72 | + spacing-top-lg: 3.2rem, | |
| 73 | + spacing-top-md: 2rem, | |
| 74 | + spacing-top-sm: 1.2rem, | |
| 75 | + spacing-top-none: 0, | |
| 76 | + spacing-bottom-xlg: 4.8rem, | |
| 77 | + spacing-bottom-lg: 2.8rem, | |
| 78 | + spacing-bottom-md: 2rem, | |
| 79 | + spacing-bottom-sm: 1.2rem, | |
| 80 | + spacing-bottom-none: 0, | |
| 81 | + section-spacing-lg: 8rem, | |
| 82 | + section-spacing-md: 4rem, | |
| 83 | + section-spacing-sm: 2.4rem, | |
| 84 | + section-spacing-xsm: 1.6rem | |
| 85 | +); | |
| 86 | + | |
| 87 | +$responsive-pc: ( | |
| 88 | + font-size-display-lg: 4rem, | |
| 89 | + font-size-display-md: 3.2rem, | |
| 90 | + font-size-display-sm: 2.6rem, | |
| 91 | + font-size-heading-xlg: 3.2rem, | |
| 92 | + font-size-heading-lg: 2.6rem, | |
| 93 | + font-size-heading-md: 2rem, | |
| 94 | + font-size-heading-sm: 1.7rem, | |
| 95 | + font-size-heading-xsm: 1.5rem, | |
| 96 | + font-size-heading-2xsm: 1.3rem, | |
| 97 | + font-size-body-lg: 1.7rem, | |
| 98 | + font-size-body-md: 1.5rem, | |
| 99 | + font-size-body-sm: 1.3rem, | |
| 100 | + font-size-body-xsm: 1.1rem, | |
| 101 | + font-size-label-lg: 1.7rem, | |
| 102 | + font-size-label-md: 1.5rem, | |
| 103 | + font-size-label-sm: 1.3rem, | |
| 104 | + font-size-label-xsm: 1.1rem, | |
| 105 | + form-radius-xsm: 0.4rem, | |
| 106 | + form-radius-sm: 0.4rem, | |
| 107 | + form-radius-md: 0.8rem, | |
| 108 | + form-radius-lg: 1.2rem, | |
| 109 | + form-padding-lg: 2rem, | |
| 110 | + form-padding-md: 1.2rem, | |
| 111 | + form-padding-sm: 0.8rem, | |
| 112 | + form-padding-xsm: 0.8rem, | |
| 113 | + form-icon-lg: 2.4rem, | |
| 114 | + form-icon-md: 2rem, | |
| 115 | + form-icon-sm: 1.6rem, | |
| 116 | + form-icon-xsm: 1.2rem, | |
| 117 | + form-height-xsm: 2.4rem, | |
| 118 | + form-height-sm: 2.8rem, | |
| 119 | + form-height-md: 4rem, | |
| 120 | + form-height-lg: 5.6rem, | |
| 121 | + grid-wrap-full: 192rem, | |
| 122 | + grid-wrap-default: 158.4rem, | |
| 123 | + grid-wrap-sm: 108rem, | |
| 124 | + grid-wrap-xsm: 80rem, | |
| 125 | + grid-margin-full: 0, | |
| 126 | + grid-margin-default: 4.8rem, | |
| 127 | + grid-margin-sm: 42rem, | |
| 128 | + grid-margin-xsm: 56rem, | |
| 129 | + grid-gutter-sm: 0.8rem, | |
| 130 | + grid-gutter-md: 1.6rem, | |
| 131 | + grid-gutter-lg: 2.4rem, | |
| 132 | + grid-gutter-xlg: 4rem, | |
| 133 | + grid-gutter-2xlg: 6.4rem, | |
| 134 | + card-radius-xlg: 2.4rem, | |
| 135 | + card-radius-lg: 2.4rem, | |
| 136 | + card-radius-md: 1.6rem, | |
| 137 | + card-radius-sm: 1.2rem, | |
| 138 | + card-padding-xlg: 4rem, | |
| 139 | + card-padding-lg: 3.2rem, | |
| 140 | + card-padding-md: 2.4rem, | |
| 141 | + card-padding-sm: 1.6rem, | |
| 142 | + modal-width-full: 192rem, | |
| 143 | + modal-width-lg: 76rem, | |
| 144 | + modal-width-md: 56rem, | |
| 145 | + modal-width-sm: 40rem, | |
| 146 | + modal-padding-default: 4rem, | |
| 147 | + modal-btn-close: 6.4rem, | |
| 148 | + spacing-top-2xlg: 8rem, | |
| 149 | + spacing-top-xlg: 6.4rem, | |
| 150 | + spacing-top-lg: 4rem, | |
| 151 | + spacing-top-md: 2.4rem, | |
| 152 | + spacing-top-sm: 1.6rem, | |
| 153 | + spacing-top-none: 0, | |
| 154 | + spacing-bottom-xlg: 3.6rem, | |
| 155 | + spacing-bottom-lg: 2.8rem, | |
| 156 | + spacing-bottom-md: 2rem, | |
| 157 | + spacing-bottom-sm: 1.2rem, | |
| 158 | + spacing-bottom-none: 0, | |
| 159 | + section-spacing-lg: 12rem, | |
| 160 | + section-spacing-md: 8rem, | |
| 161 | + section-spacing-sm: 4rem, | |
| 162 | + section-spacing-xsm: 2.4rem | |
| 163 | +); |
+++ @fox/styles/tokens/_size.scss
... | ... | @@ -0,0 +1,62 @@ |
| 1 | +// ⚠️ 이 파일은 Figma Design Tokens export에서 **자동 생성**되었습니다. | |
| 2 | +// 원본: size.tokens.json | |
| 3 | +// 손으로 고치지 말고 원본 JSON을 다시 export한 뒤 변환기를 재실행하세요. | |
| 4 | + | |
| 5 | +// 모드와 무관한 크기 스케일. 값은 1rem = 10px 기준 rem. | |
| 6 | +$size: ( | |
| 7 | + border-0: 0, | |
| 8 | + border-1: 0.1rem, | |
| 9 | + border-2: 0.2rem, | |
| 10 | + border-3: 0.4rem, | |
| 11 | + padding-1: 0.2rem, | |
| 12 | + padding-2: 0.4rem, | |
| 13 | + padding-3: 0.8rem, | |
| 14 | + padding-4: 1rem, | |
| 15 | + padding-5: 1.2rem, | |
| 16 | + padding-6: 1.6rem, | |
| 17 | + padding-7: 2rem, | |
| 18 | + padding-8: 2.4rem, | |
| 19 | + padding-9: 3.2rem, | |
| 20 | + padding-10: 4rem, | |
| 21 | + padding-11: 6.4rem, | |
| 22 | + gap-1: 0.2rem, | |
| 23 | + gap-2: 0.4rem, | |
| 24 | + gap-3: 0.8rem, | |
| 25 | + gap-4: 1.2rem, | |
| 26 | + gap-5: 1.6rem, | |
| 27 | + gap-6: 2rem, | |
| 28 | + gap-7: 2.4rem, | |
| 29 | + gap-8: 3.2rem, | |
| 30 | + gap-9: 4rem, | |
| 31 | + gap-10: 4.8rem, | |
| 32 | + gap-11: 6.4rem, | |
| 33 | + gap-12: 8rem, | |
| 34 | + radius-1: 0.2rem, | |
| 35 | + radius-2: 0.4rem, | |
| 36 | + radius-3: 0.8rem, | |
| 37 | + radius-4: 1.2rem, | |
| 38 | + radius-5: 1.6rem, | |
| 39 | + radius-6: 2.4rem, | |
| 40 | + radius-7: 4rem, | |
| 41 | + radius-max: 100rem, | |
| 42 | + icon-1: 0.8rem, | |
| 43 | + icon-2: 1.2rem, | |
| 44 | + icon-3: 1.6rem, | |
| 45 | + icon-4: 2rem, | |
| 46 | + icon-5: 2.4rem, | |
| 47 | + icon-6: 3.2rem, | |
| 48 | + icon-7: 4rem, | |
| 49 | + shadow-location1: 0.1rem, | |
| 50 | + shadow-location2: 0.2rem, | |
| 51 | + shadow-location3: 0.4rem, | |
| 52 | + shadow-location4: 0.8rem, | |
| 53 | + shadow-location5: 1.6rem, | |
| 54 | + shadow-blur1: 0.2rem, | |
| 55 | + shadow-blur2: 0.4rem, | |
| 56 | + shadow-blur3: 0.8rem, | |
| 57 | + shadow-blur4: 1.6rem, | |
| 58 | + shadow-blur5: 3.2rem, | |
| 59 | + backdrop-blur1: 1rem, | |
| 60 | + backdrop-blur2: 2.4rem, | |
| 61 | + backdrop-blur3: 4rem | |
| 62 | +); |
+++ @fox/styles/tokens/_theme.scss
... | ... | @@ -0,0 +1,40 @@ |
| 1 | +// ⚠️ 이 파일은 Figma Design Tokens export에서 **자동 생성**되었습니다. | |
| 2 | +// 원본: theme.tokens.json | |
| 3 | +// 손으로 고치지 말고 원본 JSON을 다시 export한 뒤 변환기를 재실행하세요. | |
| 4 | + | |
| 5 | +// 브랜드 램프 — primary / secondary / accent. | |
| 6 | +$theme: ( | |
| 7 | + primary-5: #ECF2FE, | |
| 8 | + primary-10: #D8E5FD, | |
| 9 | + primary-20: #B1CEFB, | |
| 10 | + primary-30: #86AFF9, | |
| 11 | + primary-40: #4C87F6, | |
| 12 | + primary-50: #256EF4, | |
| 13 | + primary-60: #0B50D0, | |
| 14 | + primary-70: #083891, | |
| 15 | + primary-80: #052561, | |
| 16 | + primary-90: #03163A, | |
| 17 | + primary-95: #020F27, | |
| 18 | + secondary-5: #EEF2F7, | |
| 19 | + secondary-10: #D6E0EB, | |
| 20 | + secondary-20: #BACBDE, | |
| 21 | + secondary-30: #90B0D5, | |
| 22 | + secondary-40: #6B96C7, | |
| 23 | + secondary-50: #346FB2, | |
| 24 | + secondary-60: #1C589C, | |
| 25 | + secondary-70: #063A74, | |
| 26 | + secondary-80: #052B57, | |
| 27 | + secondary-90: #031F3F, | |
| 28 | + secondary-95: #02162C, | |
| 29 | + accent-5: #FBEFF0, | |
| 30 | + accent-10: #F5D6D9, | |
| 31 | + accent-20: #EBADB2, | |
| 32 | + accent-30: #E0858C, | |
| 33 | + accent-40: #D65C66, | |
| 34 | + accent-50: #D63D4A, | |
| 35 | + accent-60: #AB2B36, | |
| 36 | + accent-70: #7A1F26, | |
| 37 | + accent-80: #521419, | |
| 38 | + accent-90: #310C0F, | |
| 39 | + accent-95: #21080A | |
| 40 | +); |
+++ @fox/tools/build-tokens.py
... | ... | @@ -0,0 +1,486 @@ |
| 1 | +"""Figma Design Tokens(JSON) → @fox/styles SCSS 토큰 변환기. | |
| 2 | + | |
| 3 | +입력: $FOX_TOKENS_SRC/*.tokens.json (기본 ~/Downloads/mode · 7개, Figma 컬렉션 5개/모드 7개) | |
| 4 | +출력: @fox/styles/tokens/*.scss + _root.scss + _functions.scss + _mixins.scss | |
| 5 | + | |
| 6 | +실행: python3 @fox/tools/build-tokens.py | |
| 7 | + | |
| 8 | +규칙 | |
| 9 | +- 길이 값은 1rem = 10px 기준으로 value/10 → rem. font.weight만 무단위. | |
| 10 | +- alpha < 1 색상은 rgb(R G B / A)로 출력(hex는 알파를 담지 못한다). | |
| 11 | +- Figma의 alias(참조)는 CSS에서도 var() 참조로 보존한다 — 원본 계층이 유지되어 | |
| 12 | + primitive를 바꾸면 semantic까지 전파된다. 단, 참조 대상이 제공된 파일에 없거나 | |
| 13 | + 값이 일치하지 않으면 리터럴로 떨어뜨린다(무결성 우선). | |
| 14 | +""" | |
| 15 | + | |
| 16 | +import json | |
| 17 | +import os | |
| 18 | +from collections import OrderedDict | |
| 19 | + | |
| 20 | +SRC = os.environ.get("FOX_TOKENS_SRC", os.path.expanduser("~/Downloads/mode")) | |
| 21 | +OUT = os.environ.get("FOX_OUT", os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "styles")) | |
| 22 | + | |
| 23 | +FILES = { | |
| 24 | + "primitive": "primitive.tokens.json", | |
| 25 | + "theme": "theme.tokens.json", | |
| 26 | + "size": "size.tokens.json", | |
| 27 | + "light": "light.tokens.json", | |
| 28 | + "dark": "dark.tokens.json", | |
| 29 | + "pc": "pc.tokens.json", | |
| 30 | + "mobile": "mobile.tokens.json", | |
| 31 | +} | |
| 32 | + | |
| 33 | + | |
| 34 | +def leaves(node, path=""): | |
| 35 | + if isinstance(node, dict): | |
| 36 | + if "$type" in node: | |
| 37 | + yield path, node | |
| 38 | + else: | |
| 39 | + for k, v in node.items(): | |
| 40 | + if k == "$extensions": | |
| 41 | + continue | |
| 42 | + yield from leaves(v, f"{path}.{k}" if path else k) | |
| 43 | + | |
| 44 | + | |
| 45 | +raw = {} | |
| 46 | +for key, fname in FILES.items(): | |
| 47 | + doc = json.load(open(os.path.join(SRC, fname), encoding="utf-8")) | |
| 48 | + raw[key] = OrderedDict(leaves(doc)) | |
| 49 | + | |
| 50 | + | |
| 51 | +def resolve_refs(table): | |
| 52 | + """같은 파일 안의 {a.b.c} 참조를 실제 값으로 치환한다.""" | |
| 53 | + for path, node in table.items(): | |
| 54 | + val = node["$value"] | |
| 55 | + seen = set() | |
| 56 | + while isinstance(val, str) and val.startswith("{") and val.endswith("}"): | |
| 57 | + target = val[1:-1] | |
| 58 | + if target in seen or target not in table: | |
| 59 | + raise SystemExit(f"참조 해결 실패: {path} -> {val}") | |
| 60 | + seen.add(target) | |
| 61 | + val = table[target]["$value"] | |
| 62 | + node["$value"] = val | |
| 63 | + | |
| 64 | + | |
| 65 | +for table in raw.values(): | |
| 66 | + resolve_refs(table) | |
| 67 | + | |
| 68 | + | |
| 69 | +def css_color(value): | |
| 70 | + comps = value["components"] | |
| 71 | + alpha = value.get("alpha", 1) | |
| 72 | + if alpha >= 1: | |
| 73 | + return value["hex"].upper() | |
| 74 | + r, g, b = (round(c * 255) for c in comps) | |
| 75 | + a = round(alpha, 4) | |
| 76 | + return f"rgb({r} {g} {b} / {a})" | |
| 77 | + | |
| 78 | + | |
| 79 | +def css_len(value): | |
| 80 | + """px 수치를 rem으로. 1rem = 10px.""" | |
| 81 | + if value == 0: | |
| 82 | + return "0" | |
| 83 | + rem = value / 10 | |
| 84 | + text = f"{rem:.10f}".rstrip("0").rstrip(".") | |
| 85 | + return f"{text}rem" | |
| 86 | + | |
| 87 | + | |
| 88 | +# ── 변수명 매핑 ─────────────────────────────────────────────────────────────── | |
| 89 | +def name_primitive(path): | |
| 90 | + p = path.split(".") | |
| 91 | + if p[0] == "color": | |
| 92 | + return f"primitive-{p[1]}-{p[2]}" | |
| 93 | + if p[0] == "font": | |
| 94 | + return f"font-{p[1]}-{p[2]}" | |
| 95 | + if p[0] == "number": | |
| 96 | + return f"number-{p[1]}" | |
| 97 | + raise SystemExit(f"primitive 이름 매핑 없음: {path}") | |
| 98 | + | |
| 99 | + | |
| 100 | +def name_theme(path): | |
| 101 | + p = path.split(".") | |
| 102 | + assert p[0] == "color" | |
| 103 | + return f"theme-{p[1]}-{p[2]}" | |
| 104 | + | |
| 105 | + | |
| 106 | +def name_size(path): | |
| 107 | + p = path.split(".") | |
| 108 | + if p[0] == "effect": | |
| 109 | + return f"{p[1]}-{p[2]}" # effect.shadow.blur3 → shadow-blur3 | |
| 110 | + return "-".join(p) # padding.6 → padding-6 | |
| 111 | + | |
| 112 | + | |
| 113 | +CHART_ALT_GROUP = {} | |
| 114 | +for k in [str(i) for i in range(1, 11)]: | |
| 115 | + CHART_ALT_GROUP[k] = f"chart-alt-level-{k}" | |
| 116 | +for k in ("bar", "surface", "line", "disabled"): | |
| 117 | + CHART_ALT_GROUP[k] = f"chart-alt-default-{k}" | |
| 118 | +for i in range(1, 6): | |
| 119 | + for s in ("bar", "line", "surface"): | |
| 120 | + CHART_ALT_GROUP[f"{i}-{s}"] = f"chart-alt-type-{i}-{s}" | |
| 121 | + | |
| 122 | + | |
| 123 | +def name_semantic(path): | |
| 124 | + if path in CHART_ALT_GROUP: | |
| 125 | + return f"color-{CHART_ALT_GROUP[path]}" | |
| 126 | + p = path.split(".") | |
| 127 | + assert p[0] == "color", path | |
| 128 | + return "color-" + "-".join(p[1:]) | |
| 129 | + | |
| 130 | + | |
| 131 | +def name_responsive(path): | |
| 132 | + p = path.split(".") | |
| 133 | + if p[0] == "font" and p[1] == "size": | |
| 134 | + return "font-size-" + "-".join(p[2:]) # font.size.body.md → font-size-body-md | |
| 135 | + return "-".join(p) # form.height.md → form-height-md | |
| 136 | + | |
| 137 | + | |
| 138 | +# ── alias 역참조 테이블 (Figma 변수명 → 우리 CSS 변수명) ────────────────────── | |
| 139 | +alias_lookup = {} # (컬렉션명, "color/neutral/0") → (varname, 원본값) | |
| 140 | +for coll, namer in (("primitive", name_primitive), ("theme", name_theme), ("size", name_size)): | |
| 141 | + for path, node in raw[coll].items(): | |
| 142 | + alias_lookup[(coll, path.replace(".", "/"))] = (namer(path), node["$value"]) | |
| 143 | + | |
| 144 | + | |
| 145 | +def same_value(a, b): | |
| 146 | + if isinstance(a, dict) and isinstance(b, dict): | |
| 147 | + return css_color(a) == css_color(b) | |
| 148 | + return a == b | |
| 149 | + | |
| 150 | + | |
| 151 | +stats = {"var": 0, "literal": 0, "no-alias": 0} | |
| 152 | + | |
| 153 | + | |
| 154 | +def color_expr(node): | |
| 155 | + """alias가 검증되면 var() 참조, 아니면 리터럴 색상.""" | |
| 156 | + ext = node.get("$extensions", {}) | |
| 157 | + alias = ext.get("com.figma.aliasData") | |
| 158 | + if alias: | |
| 159 | + key = (alias["targetVariableSetName"], alias["targetVariableName"]) | |
| 160 | + hit = alias_lookup.get(key) | |
| 161 | + if hit and same_value(hit[1], node["$value"]): | |
| 162 | + stats["var"] += 1 | |
| 163 | + return f"var(--fox-{hit[0]})" | |
| 164 | + stats["literal"] += 1 | |
| 165 | + else: | |
| 166 | + stats["no-alias"] += 1 | |
| 167 | + return css_color(node["$value"]) | |
| 168 | + | |
| 169 | + | |
| 170 | +# ── 맵 생성 ────────────────────────────────────────────────────────────────── | |
| 171 | +def build(entries): | |
| 172 | + return OrderedDict(entries) | |
| 173 | + | |
| 174 | + | |
| 175 | +primitive_map, font_map, number_map = OrderedDict(), OrderedDict(), OrderedDict() | |
| 176 | +for path, node in raw["primitive"].items(): | |
| 177 | + name = name_primitive(path) | |
| 178 | + if name.startswith("primitive-"): | |
| 179 | + primitive_map[name[len("primitive-"):]] = css_color(node["$value"]) | |
| 180 | + elif name.startswith("font-"): | |
| 181 | + v = node["$value"] | |
| 182 | + font_map[name[len("font-"):]] = v if isinstance(v, (str, int)) else css_color(v) | |
| 183 | + else: | |
| 184 | + number_map[name[len("number-"):]] = css_len(node["$value"]) | |
| 185 | + | |
| 186 | +theme_map = OrderedDict( | |
| 187 | + (name_theme(p)[len("theme-"):], css_color(n["$value"])) for p, n in raw["theme"].items() | |
| 188 | +) | |
| 189 | + | |
| 190 | +size_map = OrderedDict((name_size(p), css_len(n["$value"])) for p, n in raw["size"].items()) | |
| 191 | + | |
| 192 | +# 시맨틱 색상: light/dark 를 light-dark() 한 줄로. | |
| 193 | +# 소속이 불분명해 chart-alt로 묶은 29개는 뒤로 보낸다 — 원본 JSON에서는 이들이 루트에 | |
| 194 | +# 먼저 오지만, 순서에 의미가 없고 카탈로그 첫 화면이 주요 색상이어야 읽기 쉽다. | |
| 195 | +color_map = OrderedDict() | |
| 196 | +ordered_light = sorted(raw["light"].items(), key=lambda kv: kv[0] in CHART_ALT_GROUP) | |
| 197 | +for path, lnode in ordered_light: | |
| 198 | + dnode = raw["dark"][path] | |
| 199 | + key = name_semantic(path)[len("color-"):] | |
| 200 | + color_map[key] = f"light-dark({color_expr(lnode)}, {color_expr(dnode)})" | |
| 201 | + | |
| 202 | +# 반응형: 모바일 기본 + pc 오버라이드 | |
| 203 | +resp_mobile = OrderedDict( | |
| 204 | + (name_responsive(p), css_len(n["$value"])) for p, n in raw["mobile"].items() | |
| 205 | +) | |
| 206 | +resp_pc = OrderedDict((name_responsive(p), css_len(n["$value"])) for p, n in raw["pc"].items()) | |
| 207 | +assert list(resp_mobile) == list(resp_pc) | |
| 208 | + | |
| 209 | +BREAKPOINT_PC = "768px" | |
| 210 | + | |
| 211 | + | |
| 212 | +# ── SCSS 출력 ──────────────────────────────────────────────────────────────── | |
| 213 | +def scss_map(name, mapping, indent=" "): | |
| 214 | + lines = [f"${name}: ("] | |
| 215 | + items = list(mapping.items()) | |
| 216 | + for i, (k, v) in enumerate(items): | |
| 217 | + comma = "," if i < len(items) - 1 else "" | |
| 218 | + key = f'"{k}"' if not k.replace("-", "").replace("_", "").isalnum() else k | |
| 219 | + lines.append(f"{indent}{key}: {v}{comma}") | |
| 220 | + lines.append(");") | |
| 221 | + return "\n".join(lines) | |
| 222 | + | |
| 223 | + | |
| 224 | +HEADER = """// ⚠️ 이 파일은 Figma Design Tokens export에서 **자동 생성**되었습니다. | |
| 225 | +// 원본: {src} | |
| 226 | +// 손으로 고치지 말고 원본 JSON을 다시 export한 뒤 변환기를 재실행하세요. | |
| 227 | +""" | |
| 228 | + | |
| 229 | + | |
| 230 | +def write(rel, body): | |
| 231 | + path = os.path.join(OUT, rel) | |
| 232 | + os.makedirs(os.path.dirname(path), exist_ok=True) | |
| 233 | + open(path, "w", encoding="utf-8").write(body) | |
| 234 | + return rel | |
| 235 | + | |
| 236 | + | |
| 237 | +written = [] | |
| 238 | + | |
| 239 | +written.append(write("tokens/_primitive.scss", HEADER.format(src="primitive.tokens.json") + f""" | |
| 240 | +// 원시 팔레트 — 화면에서 직접 쓰지 않는다. 시맨틱 토큰(`$color`)이 참조하는 바닥층이다. | |
| 241 | +{scss_map("primitive", primitive_map)} | |
| 242 | + | |
| 243 | +// 서체 — family는 문자열, weight는 무단위 수치(길이가 아니므로 rem 변환 대상이 아니다). | |
| 244 | +{scss_map("font", font_map)} | |
| 245 | + | |
| 246 | +// 원시 수치 스케일 — size 컬렉션이 참조하는 값들. 1rem = 10px 기준으로 변환됨. | |
| 247 | +{scss_map("number", number_map)} | |
| 248 | +""")) | |
| 249 | + | |
| 250 | +written.append(write("tokens/_theme.scss", HEADER.format(src="theme.tokens.json") + f""" | |
| 251 | +// 브랜드 램프 — primary / secondary / accent. | |
| 252 | +{scss_map("theme", theme_map)} | |
| 253 | +""")) | |
| 254 | + | |
| 255 | +written.append(write("tokens/_size.scss", HEADER.format(src="size.tokens.json") + f""" | |
| 256 | +// 모드와 무관한 크기 스케일. 값은 1rem = 10px 기준 rem. | |
| 257 | +{scss_map("size", size_map)} | |
| 258 | +""")) | |
| 259 | + | |
| 260 | +written.append(write("tokens/_color.scss", HEADER.format(src="light.tokens.json / dark.tokens.json") + f""" | |
| 261 | +// 시맨틱 색상 — 라이트/다크 값을 `light-dark()` 한 줄에 함께 보유한다. | |
| 262 | +// 전환은 `_root.scss`의 `color-scheme`이 담당하므로 다크 전용 블록이 없고, | |
| 263 | +// 따라서 한쪽만 고쳐서 생기는 테마 불일치가 구조적으로 불가능하다. | |
| 264 | +// | |
| 265 | +// 값이 `var(--fox-primitive-*)` / `var(--fox-theme-*)`인 항목은 Figma의 alias를 | |
| 266 | +// 그대로 옮긴 것이다 — 바닥층을 바꾸면 여기까지 전파된다. | |
| 267 | +{scss_map("color", color_map)} | |
| 268 | +""")) | |
| 269 | + | |
| 270 | +written.append(write("tokens/_responsive.scss", HEADER.format(src="mobile.tokens.json / pc.tokens.json") + f""" | |
| 271 | +// 뷰포트에 따라 달라지는 크기 토큰. 키는 두 모드가 완전히 동일하다. | |
| 272 | +// 모바일 우선 — `$responsive`가 기본값이고 `$responsive-pc`가 브레이크포인트 이상에서 덮는다. | |
| 273 | +$breakpoint-pc: {BREAKPOINT_PC}; | |
| 274 | + | |
| 275 | +{scss_map("responsive", resp_mobile)} | |
| 276 | + | |
| 277 | +{scss_map("responsive-pc", resp_pc)} | |
| 278 | +""")) | |
| 279 | + | |
| 280 | +written.append(write("tokens/_index.scss", """// 토큰 map 모음 — **CSS를 한 줄도 출력하지 않는다.** | |
| 281 | +// 값 선언(이 폴더)과 CSS 출력(`../_root.scss`)을 분리해 두었기 때문에, 컴포넌트의 | |
| 282 | +// `.module.scss`가 몇 개든 `abstracts`를 `@use`해도 `:root` 블록이 중복 출력되지 않는다 | |
| 283 | +// (각 CSS Module은 별도 컴파일 단위라 CSS를 내보내는 모듈을 공유하면 파일마다 복제된다). | |
| 284 | + | |
| 285 | +@forward "primitive"; | |
| 286 | +@forward "theme"; | |
| 287 | +@forward "size"; | |
| 288 | +@forward "color"; | |
| 289 | +@forward "responsive"; | |
| 290 | +""")) | |
| 291 | + | |
| 292 | +# _root.scss | |
| 293 | +root_lines = [] | |
| 294 | +root_lines.append("""// 토큰 CSS 출력 — `tokens/`의 SCSS map을 `--fox-*` 커스텀 프로퍼티로 선언한다. | |
| 295 | +// | |
| 296 | +// **앱 전체에서 딱 한 번만 로드되어야 한다** (`index.scss` 경유). | |
| 297 | +// 컴포넌트의 `.module.scss`에서는 절대 `@use` 하지 않는다. | |
| 298 | +// | |
| 299 | +// map을 순회해 생성하므로 토큰을 추가할 때 이 파일은 손대지 않는다. | |
| 300 | + | |
| 301 | +@use "sass:map"; | |
| 302 | +@use "tokens"; | |
| 303 | + | |
| 304 | +:root { | |
| 305 | + // 기본은 OS 설정 추종. 아래 `[data-theme]` 규칙이 이 값만 덮어써 수동 선택을 | |
| 306 | + // 처리하므로, 색상 값 자체는 `tokens/_color.scss` 한 곳에만 존재한다. | |
| 307 | + color-scheme: light dark; | |
| 308 | +""") | |
| 309 | +for var_prefix, map_name in ( | |
| 310 | + ("primitive", "primitive"), | |
| 311 | + ("font", "font"), | |
| 312 | + ("number", "number"), | |
| 313 | + ("theme", "theme"), | |
| 314 | +): | |
| 315 | + root_lines.append(f""" @each $name, $value in tokens.${map_name} {{ | |
| 316 | + --fox-{var_prefix}-#{{$name}}: #{{$value}}; | |
| 317 | + }} | |
| 318 | +""") | |
| 319 | +root_lines.append(""" // size / color / responsive 는 이름에 그룹이 이미 들어 있어 접두사가 `--fox-` 뿐이다. | |
| 320 | + @each $name, $value in tokens.$size { | |
| 321 | + --fox-#{$name}: #{$value}; | |
| 322 | + } | |
| 323 | + | |
| 324 | + @each $name, $value in tokens.$color { | |
| 325 | + --fox-color-#{$name}: #{$value}; | |
| 326 | + } | |
| 327 | + | |
| 328 | + // 모바일 우선 기본값. | |
| 329 | + @each $name, $value in tokens.$responsive { | |
| 330 | + --fox-#{$name}: #{$value}; | |
| 331 | + } | |
| 332 | +} | |
| 333 | + | |
| 334 | +// PC 모드 — 반응형 토큰만 덮어쓴다. 색상은 뷰포트와 무관하므로 여기 오지 않는다. | |
| 335 | +@media (min-width: tokens.$breakpoint-pc) { | |
| 336 | + :root { | |
| 337 | + @each $name, $value in tokens.$responsive-pc { | |
| 338 | + --fox-#{$name}: #{$value}; | |
| 339 | + } | |
| 340 | + } | |
| 341 | +} | |
| 342 | + | |
| 343 | +// 수동 테마 선택 — `color-scheme`만 고정하면 `light-dark()`가 해당 분기로 확정된다. | |
| 344 | +// 속성이 없거나 `"system"`이면 위 `:root`의 `light dark`가 유지되어 OS를 따른다. | |
| 345 | +:root[data-theme="light"] { | |
| 346 | + color-scheme: light; | |
| 347 | +} | |
| 348 | + | |
| 349 | +:root[data-theme="dark"] { | |
| 350 | + color-scheme: dark; | |
| 351 | +} | |
| 352 | + | |
| 353 | +// ⚠️ 1rem = 10px 규약의 근거. 브라우저 기본 글자 크기(16px)의 62.5%가 10px다. | |
| 354 | +// 모든 토큰의 rem 값이 이 선언을 전제로 계산돼 있다. | |
| 355 | +// 미디어 쿼리 안의 rem은 이 값의 영향을 받지 않는다(항상 브라우저 기본 크기 기준). | |
| 356 | +html { | |
| 357 | + font-size: 62.5%; | |
| 358 | +} | |
| 359 | +""") | |
| 360 | +written.append(write("_root.scss", "\n".join(root_lines))) | |
| 361 | + | |
| 362 | +# _functions.scss | |
| 363 | +GROUPS = [ | |
| 364 | + # (함수명, 대상 map, 키 접두사(없으면 None), CSS 변수 접두사) | |
| 365 | + ("color", "color", None, "color-"), | |
| 366 | + ("primitive", "primitive", None, "primitive-"), | |
| 367 | + ("theme", "theme", None, "theme-"), | |
| 368 | + ("number", "number", None, "number-"), | |
| 369 | + ("font-family", "font", "family", "font-"), | |
| 370 | + ("font-weight", "font", "weight", "font-"), | |
| 371 | + ("border", "size", "border", ""), | |
| 372 | + ("padding", "size", "padding", ""), | |
| 373 | + ("gap", "size", "gap", ""), | |
| 374 | + ("radius", "size", "radius", ""), | |
| 375 | + ("icon", "size", "icon", ""), | |
| 376 | + ("shadow", "size", "shadow", ""), | |
| 377 | + ("backdrop", "size", "backdrop", ""), | |
| 378 | + ("font-size", "responsive", "font-size", ""), | |
| 379 | + ("form", "responsive", "form", ""), | |
| 380 | + ("grid", "responsive", "grid", ""), | |
| 381 | + ("card", "responsive", "card", ""), | |
| 382 | + ("modal", "responsive", "modal", ""), | |
| 383 | + ("spacing", "responsive", "spacing", ""), | |
| 384 | + ("section", "responsive", "section", ""), | |
| 385 | +] | |
| 386 | + | |
| 387 | +fn_lines = ["""// 토큰 접근 함수 — 디자인 시스템 규율을 **컴파일러가 강제**하는 지점. | |
| 388 | +// | |
| 389 | +// 화면 코드는 `color: #256EF4`나 `padding: 13px`를 쓸 수 없고, 반드시 | |
| 390 | +// `color: fox.color(font-neutral-strong)` / `padding: fox.padding(6)`로 토큰을 | |
| 391 | +// 지목해야 한다. 존재하지 않는 이름을 쓰면 `@error`로 **빌드가 실패**하고 사용 | |
| 392 | +// 가능한 토큰 목록이 함께 출력된다. | |
| 393 | +// | |
| 394 | +// 반환값은 CSS 커스텀 프로퍼티 참조(`var(--fox-*)`)다. 값이 아니라 참조를 돌려주므로 | |
| 395 | +// (1) 라이트/다크와 pc/모바일이 런타임에 전환되고 (2) 특정 영역만 토큰을 덮어쓰는 | |
| 396 | +// 스코프 오버라이드가 가능하며 (3) devtools에서 어떤 토큰인지 그대로 보인다. | |
| 397 | + | |
| 398 | +@use "sass:map"; | |
| 399 | +@use "tokens"; | |
| 400 | + | |
| 401 | +/// 토큰 이름을 검증하고 커스텀 프로퍼티 참조를 반환한다. | |
| 402 | +@function _token($group, $key, $registry, $var-name) { | |
| 403 | + @if not map.has-key($registry, $key) { | |
| 404 | + @error "[@fox] 알 수 없는 #{$group} 토큰: `#{$key}` — 사용 가능한 값: #{map.keys($registry)}"; | |
| 405 | + } | |
| 406 | + | |
| 407 | + @return var(--fox-#{$var-name}); | |
| 408 | +} | |
| 409 | +"""] | |
| 410 | +for fn, mapname, prefix, varprefix in GROUPS: | |
| 411 | + if prefix is None: | |
| 412 | + fn_lines.append(f""" | |
| 413 | +@function {fn}($name) {{ | |
| 414 | + @return _token("{fn}", $name, tokens.${mapname}, "{varprefix}#{{$name}}"); | |
| 415 | +}}""") | |
| 416 | + else: | |
| 417 | + fn_lines.append(f""" | |
| 418 | +@function {fn}($name) {{ | |
| 419 | + @return _token("{fn}", "{prefix}-#{{$name}}", tokens.${mapname}, "{varprefix}{prefix}-#{{$name}}"); | |
| 420 | +}}""") | |
| 421 | +written.append(write("_functions.scss", "\n".join(fn_lines) + "\n")) | |
| 422 | + | |
| 423 | +written.append(write("_mixins.scss", f"""// 믹스인 — 여러 선언이 항상 함께 가야 하는 패턴을 묶는다. | |
| 424 | + | |
| 425 | +@use "tokens"; | |
| 426 | + | |
| 427 | +/// PC 이상(>= {BREAKPOINT_PC})에서 적용. 모바일 우선이므로 기본 스타일은 밖에 쓴다. | |
| 428 | +/// | |
| 429 | +/// ⚠️ 미디어 쿼리 조건부는 `var()`를 해석하지 못하므로 브레이크포인트만은 CSS 변수가 | |
| 430 | +/// 아닌 컴파일타임 값이다. 그래서 이 믹스인 경유가 강제된다. | |
| 431 | +@mixin pc {{ | |
| 432 | + @media (min-width: tokens.$breakpoint-pc) {{ | |
| 433 | + @content; | |
| 434 | + }} | |
| 435 | +}} | |
| 436 | + | |
| 437 | +/// PC 미만에서만 적용. `pc`와 경계가 겹치지 않도록 0.02px을 뺀다. | |
| 438 | +@mixin mobile {{ | |
| 439 | + @media (max-width: tokens.$breakpoint-pc - 0.02px) {{ | |
| 440 | + @content; | |
| 441 | + }} | |
| 442 | +}} | |
| 443 | +""")) | |
| 444 | + | |
| 445 | +written.append(write("abstracts.scss", """// 저작(authoring) 진입점 — 컴포넌트의 `.module.scss`가 `@use`하는 파일. | |
| 446 | +// | |
| 447 | +// @use "@fox/styles/abstracts" as fox; | |
| 448 | +// | |
| 449 | +// .button { | |
| 450 | +// padding: fox.padding(5) fox.padding(7); | |
| 451 | +// background: fox.color(button-primary-surface); | |
| 452 | +// border-radius: fox.radius(3); | |
| 453 | +// font-size: fox.font-size(label-md); | |
| 454 | +// } | |
| 455 | +// | |
| 456 | +// ⚠️ **이 파일은 CSS를 한 줄도 출력하지 않는다.** 함수·믹스인·map 정의만 있다. | |
| 457 | +// 실제 CSS 출력(토큰 선언)은 앱이 딱 한 번 import하는 `index.scss`가 전담한다. | |
| 458 | + | |
| 459 | +@forward "tokens"; | |
| 460 | +@forward "functions"; | |
| 461 | +@forward "mixins"; | |
| 462 | +""")) | |
| 463 | + | |
| 464 | +written.append(write("index.scss", """// 글로벌 스타일 진입점 — **호스트 앱이 딱 한 번 import한다.** | |
| 465 | +// | |
| 466 | +// // app/globals.scss | |
| 467 | +// @use "@fox/styles"; | |
| 468 | +// | |
| 469 | +// 여기서만 CSS가 출력된다. 컴포넌트의 `.module.scss`는 이 파일이 아니라 | |
| 470 | +// `abstracts.scss`를 `@use`해야 한다 — 그쪽은 CSS 출력이 0이라 몇 번을 참조해도 | |
| 471 | +// 중복이 생기지 않는다. | |
| 472 | + | |
| 473 | +@use "root"; | |
| 474 | +""")) | |
| 475 | + | |
| 476 | +print("생성된 파일:") | |
| 477 | +for w in written: | |
| 478 | + print(" ", w) | |
| 479 | +print() | |
| 480 | +print(f"토큰 수 — primitive {len(primitive_map)} / font {len(font_map)} / number {len(number_map)}" | |
| 481 | + f" / theme {len(theme_map)} / size {len(size_map)} / color {len(color_map)}" | |
| 482 | + f" / responsive {len(resp_mobile)}") | |
| 483 | +print(f"색상 표현 — var() 참조 {stats['var']} · 리터럴(참조 검증 실패) {stats['literal']}" | |
| 484 | + f" · alias 없음 {stats['no-alias']}") | |
| 485 | +total = len(primitive_map) + len(font_map) + len(number_map) + len(theme_map) + len(size_map) + len(color_map) + len(resp_mobile) | |
| 486 | +print(f"총 CSS 커스텀 프로퍼티: {total}") |
--- app/design/_components/token-catalog.tsx
+++ app/design/_components/token-catalog.tsx
... | ... | @@ -2,9 +2,44 @@ |
| 2 | 2 |
|
| 3 | 3 |
import type { CSSProperties } from "react";
|
| 4 | 4 |
import { useTokenRegistry } from "../_hooks/use-token-registry";
|
| 5 |
-import { typoPresetNames, type TokenEntry } from "../_lib/read-tokens";
|
|
| 5 |
+import type { TokenEntry } from "../_lib/read-tokens";
|
|
| 6 | 6 |
import { ThemeSwitch } from "./theme-switch";
|
| 7 | 7 |
import styles from "../design.module.scss"; |
| 8 |
+ |
|
| 9 |
+/** |
|
| 10 |
+ * 렌더 방식만 다를 뿐, 목록은 전부 CSSOM에서 읽는다 — 여기에 토큰 이름을 적지 않는다. |
|
| 11 |
+ * `kind`는 그 그룹을 어떻게 보여줄지만 정한다: 색은 스와치, 길이는 막대, 나머지는 값만. |
|
| 12 |
+ */ |
|
| 13 |
+const GROUPS: { key: string; label: string; kind: "swatch" | "bar" | "value"; note?: string }[] = [
|
|
| 14 |
+ {
|
|
| 15 |
+ key: "color", |
|
| 16 |
+ label: "시맨틱 색상", |
|
| 17 |
+ kind: "swatch", |
|
| 18 |
+ note: "light/dark 값이 light-dark() 한 줄에 함께 정의돼 있습니다. 위 전환기로 두 테마를 대조해 보세요.", |
|
| 19 |
+ }, |
|
| 20 |
+ { key: "theme", label: "브랜드 램프", kind: "swatch", note: "primary · secondary · accent" },
|
|
| 21 |
+ {
|
|
| 22 |
+ key: "primitive", |
|
| 23 |
+ label: "원시 팔레트", |
|
| 24 |
+ kind: "swatch", |
|
| 25 |
+ note: "화면에서 직접 쓰지 않는 바닥층입니다. 시맨틱 색상이 이 값들을 참조합니다.", |
|
| 26 |
+ }, |
|
| 27 |
+ { key: "font", label: "서체", kind: "value", note: "size 계열은 뷰포트에 따라 값이 바뀝니다." },
|
|
| 28 |
+ { key: "padding", label: "패딩", kind: "bar" },
|
|
| 29 |
+ { key: "gap", label: "간격", kind: "bar" },
|
|
| 30 |
+ { key: "radius", label: "모서리 반경", kind: "bar" },
|
|
| 31 |
+ { key: "icon", label: "아이콘 크기", kind: "bar" },
|
|
| 32 |
+ { key: "border", label: "테두리 두께", kind: "bar" },
|
|
| 33 |
+ { key: "number", label: "원시 수치", kind: "bar" },
|
|
| 34 |
+ { key: "shadow", label: "그림자 수치", kind: "value" },
|
|
| 35 |
+ { key: "backdrop", label: "배경 흐림", kind: "value" },
|
|
| 36 |
+ { key: "form", label: "폼", kind: "value" },
|
|
| 37 |
+ { key: "card", label: "카드", kind: "value" },
|
|
| 38 |
+ { key: "modal", label: "모달", kind: "value" },
|
|
| 39 |
+ { key: "grid", label: "그리드", kind: "value" },
|
|
| 40 |
+ { key: "spacing", label: "여백", kind: "value" },
|
|
| 41 |
+ { key: "section", label: "섹션", kind: "value" },
|
|
| 42 |
+]; |
|
| 8 | 43 |
|
| 9 | 44 |
/** |
| 10 | 45 |
* ⚠️ 이 파일은 인라인 `style`을 쓰는 **유일하게 허용되는 예외**다. 카탈로그는 토큰 |
... | ... | @@ -17,41 +52,59 @@ |
| 17 | 52 |
return { [property]: `var(${token.property})` } as CSSProperties;
|
| 18 | 53 |
} |
| 19 | 54 |
|
| 20 |
-function typoRef(preset: string): CSSProperties {
|
|
| 21 |
- return {
|
|
| 22 |
- fontSize: `var(--fox-typo-${preset}-size)`,
|
|
| 23 |
- fontWeight: `var(--fox-typo-${preset}-weight)`,
|
|
| 24 |
- lineHeight: `var(--fox-typo-${preset}-line-height)`,
|
|
| 25 |
- letterSpacing: `var(--fox-typo-${preset}-letter-spacing)`,
|
|
| 26 |
- }; |
|
| 55 |
+function Swatches({ tokens, fn }: { tokens: TokenEntry[]; fn: string }) {
|
|
| 56 |
+ return ( |
|
| 57 |
+ <div className={styles.grid}>
|
|
| 58 |
+ {tokens.map((token) => (
|
|
| 59 |
+ <div key={token.property} className={styles.swatch}>
|
|
| 60 |
+ <div className={styles.swatchChip} style={ref("background", token)} />
|
|
| 61 |
+ <div className={styles.swatchMeta}>
|
|
| 62 |
+ <span className={styles.name}>
|
|
| 63 |
+ fox.{fn}({token.name})
|
|
| 64 |
+ </span> |
|
| 65 |
+ <span className={styles.ref}>{token.value}</span>
|
|
| 66 |
+ </div> |
|
| 67 |
+ </div> |
|
| 68 |
+ ))} |
|
| 69 |
+ </div> |
|
| 70 |
+ ); |
|
| 27 | 71 |
} |
| 28 | 72 |
|
| 29 |
-function ValueTable({ rows }: { rows: { label: string; value: string }[] }) {
|
|
| 73 |
+function ValueTable({
|
|
| 74 |
+ tokens, |
|
| 75 |
+ fn, |
|
| 76 |
+ withBar, |
|
| 77 |
+}: {
|
|
| 78 |
+ tokens: TokenEntry[]; |
|
| 79 |
+ fn: string; |
|
| 80 |
+ withBar: boolean; |
|
| 81 |
+}) {
|
|
| 30 | 82 |
return ( |
| 31 | 83 |
<table className={styles.table}>
|
| 32 | 84 |
<thead> |
| 33 | 85 |
<tr> |
| 34 | 86 |
<th scope="col">토큰</th> |
| 35 | 87 |
<th scope="col">값</th> |
| 88 |
+ {withBar ? <th scope="col">크기</th> : null}
|
|
| 36 | 89 |
</tr> |
| 37 | 90 |
</thead> |
| 38 | 91 |
<tbody> |
| 39 |
- {rows.map((row) => (
|
|
| 40 |
- <tr key={row.label}>
|
|
| 41 |
- <td>{row.label}</td>
|
|
| 42 |
- <td>{row.value}</td>
|
|
| 92 |
+ {tokens.map((token) => (
|
|
| 93 |
+ <tr key={token.property}>
|
|
| 94 |
+ <td> |
|
| 95 |
+ fox.{fn}({token.name})
|
|
| 96 |
+ </td> |
|
| 97 |
+ <td className={styles.tdValue}>{token.value}</td>
|
|
| 98 |
+ {withBar ? (
|
|
| 99 |
+ <td> |
|
| 100 |
+ <span className={styles.bar} style={ref("inlineSize", token)} />
|
|
| 101 |
+ </td> |
|
| 102 |
+ ) : null} |
|
| 43 | 103 |
</tr> |
| 44 | 104 |
))} |
| 45 | 105 |
</tbody> |
| 46 | 106 |
</table> |
| 47 | 107 |
); |
| 48 |
-} |
|
| 49 |
- |
|
| 50 |
-function rows(group: TokenEntry[] | undefined, fn: string) {
|
|
| 51 |
- return (group ?? []).map((token) => ({
|
|
| 52 |
- label: `fox.${fn}(${token.name})`,
|
|
| 53 |
- value: token.value, |
|
| 54 |
- })); |
|
| 55 | 108 |
} |
| 56 | 109 |
|
| 57 | 110 |
export function TokenCatalog() {
|
... | ... | @@ -67,11 +120,7 @@ |
| 67 | 120 |
); |
| 68 | 121 |
} |
| 69 | 122 |
|
| 70 |
- const colors = registry.color ?? []; |
|
| 71 |
- const spaces = registry.space ?? []; |
|
| 72 |
- const radii = registry.radius ?? []; |
|
| 73 |
- const shadows = registry.shadow ?? []; |
|
| 74 |
- const presets = typoPresetNames(registry); |
|
| 123 |
+ const total = Object.values(registry).reduce((sum, list) => sum + list.length, 0); |
|
| 75 | 124 |
|
| 76 | 125 |
return ( |
| 77 | 126 |
<main className={styles.page}>
|
... | ... | @@ -79,109 +128,33 @@ |
| 79 | 128 |
<div> |
| 80 | 129 |
<h1 className={styles.title}>디자인 시스템 카탈로그</h1>
|
| 81 | 130 |
<p className={styles.lede}>
|
| 82 |
- @fox/styles/tokens 전량을 CSSOM에서 읽어 그립니다. 개발 환경에서만 열립니다. |
|
| 131 |
+ @fox/styles/tokens 전량 {total}개를 CSSOM에서 읽어 그립니다. 개발 환경에서만
|
|
| 132 |
+ 열립니다. 반응형 토큰은 현재 창 크기 기준 값입니다. |
|
| 83 | 133 |
</p> |
| 84 | 134 |
</div> |
| 85 | 135 |
<ThemeSwitch /> |
| 86 | 136 |
</header> |
| 87 | 137 |
|
| 88 |
- <section className={styles.section}>
|
|
| 89 |
- <h2 className={styles.sectionTitle}>색상 · {colors.length}</h2>
|
|
| 90 |
- <p className={styles.sectionNote}>
|
|
| 91 |
- 라이트/다크 값이 light-dark() 한 줄에 함께 정의돼 있습니다. 위 전환기로 두 테마를 |
|
| 92 |
- 대조해 보세요. |
|
| 93 |
- </p> |
|
| 94 |
- <div className={styles.grid}>
|
|
| 95 |
- {colors.map((token) => (
|
|
| 96 |
- <div key={token.property} className={styles.swatch}>
|
|
| 97 |
- <div className={styles.swatchChip} style={ref("background", token)} />
|
|
| 98 |
- <div className={styles.swatchMeta}>
|
|
| 99 |
- <span className={styles.name}>fox.color({token.name})</span>
|
|
| 100 |
- <span className={styles.ref}>{token.value}</span>
|
|
| 101 |
- </div> |
|
| 102 |
- </div> |
|
| 103 |
- ))} |
|
| 104 |
- </div> |
|
| 105 |
- </section> |
|
| 138 |
+ {GROUPS.map(({ key, label, kind, note }) => {
|
|
| 139 |
+ const tokens = registry[key]; |
|
| 140 |
+ if (!tokens?.length) {
|
|
| 141 |
+ return null; |
|
| 142 |
+ } |
|
| 106 | 143 |
|
| 107 |
- <section className={styles.section}>
|
|
| 108 |
- <h2 className={styles.sectionTitle}>타이포 프리셋 · {presets.length}</h2>
|
|
| 109 |
- <p className={styles.sectionNote}>
|
|
| 110 |
- 크기·굵기·행간·자간 4종이 한 묶음입니다. @include fox.typo(이름)으로 적용합니다. |
|
| 111 |
- </p> |
|
| 112 |
- {presets.map((preset) => (
|
|
| 113 |
- <div key={preset} className={styles.typoRow}>
|
|
| 114 |
- <span className={styles.typoName}>{preset}</span>
|
|
| 115 |
- <span className={styles.typoSample} style={typoRef(preset)}>
|
|
| 116 |
- 다람쥐 헌 쳇바퀴에 타고파 · Handgloves 0123 |
|
| 117 |
- </span> |
|
| 118 |
- </div> |
|
| 119 |
- ))} |
|
| 120 |
- </section> |
|
| 121 |
- |
|
| 122 |
- <section className={styles.section}>
|
|
| 123 |
- <h2 className={styles.sectionTitle}>간격 · {spaces.length}</h2>
|
|
| 124 |
- <p className={styles.sectionNote}>
|
|
| 125 |
- 4px 배수 스케일. 1rem = 10px이므로 fox.space(4) = 1.6rem = 16px입니다. |
|
| 126 |
- </p> |
|
| 127 |
- {spaces.map((token) => (
|
|
| 128 |
- <div key={token.property} className={styles.spaceRow}>
|
|
| 129 |
- <span className={styles.typoName}>
|
|
| 130 |
- space({token.name}) · {token.value}
|
|
| 131 |
- </span> |
|
| 132 |
- <span className={styles.spaceBar} style={ref("inlineSize", token)} />
|
|
| 133 |
- </div> |
|
| 134 |
- ))} |
|
| 135 |
- </section> |
|
| 136 |
- |
|
| 137 |
- <section className={styles.section}>
|
|
| 138 |
- <h2 className={styles.sectionTitle}>모서리 반경 · {radii.length}</h2>
|
|
| 139 |
- <div className={styles.grid}>
|
|
| 140 |
- {radii.map((token) => (
|
|
| 141 |
- <div key={token.property} className={styles.tile} style={ref("borderRadius", token)}>
|
|
| 142 |
- {token.name} · {token.value}
|
|
| 143 |
- </div> |
|
| 144 |
- ))} |
|
| 145 |
- </div> |
|
| 146 |
- </section> |
|
| 147 |
- |
|
| 148 |
- <section className={styles.section}>
|
|
| 149 |
- <h2 className={styles.sectionTitle}>그림자 · {shadows.length}</h2>
|
|
| 150 |
- <p className={styles.sectionNote}>크기가 아니라 쓰임(고도)으로 이름 붙였습니다.</p>
|
|
| 151 |
- <div className={styles.grid}>
|
|
| 152 |
- {shadows.map((token) => (
|
|
| 153 |
- <div key={token.property} className={styles.shadowCell}>
|
|
| 154 |
- <div className={styles.shadowTile} style={ref("boxShadow", token)}>
|
|
| 155 |
- {token.name}
|
|
| 156 |
- </div> |
|
| 157 |
- </div> |
|
| 158 |
- ))} |
|
| 159 |
- </div> |
|
| 160 |
- </section> |
|
| 161 |
- |
|
| 162 |
- <section className={styles.section}>
|
|
| 163 |
- <h2 className={styles.sectionTitle}>모션</h2>
|
|
| 164 |
- <ValueTable |
|
| 165 |
- rows={[...rows(registry.duration, "duration"), ...rows(registry.easing, "easing")]}
|
|
| 166 |
- /> |
|
| 167 |
- </section> |
|
| 168 |
- |
|
| 169 |
- <section className={styles.section}>
|
|
| 170 |
- <h2 className={styles.sectionTitle}>서체 · 굵기 · z 층 · 브레이크포인트</h2>
|
|
| 171 |
- <p className={styles.sectionNote}>
|
|
| 172 |
- 브레이크포인트는 미디어 쿼리가 var()를 해석하지 못하므로 실제 사용은 fox.media() |
|
| 173 |
- 믹스인 경유입니다. 또한 미디어 쿼리 안의 rem은 html font-size의 영향을 받지 않아 |
|
| 174 |
- 48rem은 480px가 아니라 768px입니다. |
|
| 175 |
- </p> |
|
| 176 |
- <ValueTable |
|
| 177 |
- rows={[
|
|
| 178 |
- ...rows(registry.font, "font"), |
|
| 179 |
- ...rows(registry.weight, "weight"), |
|
| 180 |
- ...rows(registry.z, "z"), |
|
| 181 |
- ...rows(registry.breakpoint, "media"), |
|
| 182 |
- ]} |
|
| 183 |
- /> |
|
| 184 |
- </section> |
|
| 144 |
+ return ( |
|
| 145 |
+ <section key={key} className={styles.section}>
|
|
| 146 |
+ <h2 className={styles.sectionTitle}>
|
|
| 147 |
+ {label} · {tokens.length}
|
|
| 148 |
+ </h2> |
|
| 149 |
+ {note ? <p className={styles.sectionNote}>{note}</p> : null}
|
|
| 150 |
+ {kind === "swatch" ? (
|
|
| 151 |
+ <Swatches tokens={tokens} fn={key} />
|
|
| 152 |
+ ) : ( |
|
| 153 |
+ <ValueTable tokens={tokens} fn={key} withBar={kind === "bar"} />
|
|
| 154 |
+ )} |
|
| 155 |
+ </section> |
|
| 156 |
+ ); |
|
| 157 |
+ })} |
|
| 185 | 158 |
</main> |
| 186 | 159 |
); |
| 187 | 160 |
} |
--- app/design/_lib/read-tokens.ts
+++ app/design/_lib/read-tokens.ts
... | ... | @@ -1,12 +1,13 @@ |
| 1 | 1 |
export interface TokenEntry {
|
| 2 |
- /** 토큰 이름 (`--fox-color-primary` → `primary`) */ |
|
| 2 |
+ /** 그룹을 뗀 토큰 이름 (`--fox-color-background-default` → `background-default`) */ |
|
| 3 | 3 |
name: string; |
| 4 | 4 |
/** |
| 5 |
- * 현재 테마에서 실제로 해석된 값 (`#818cf8`). |
|
| 5 |
+ * 현재 테마·뷰포트에서 실제로 해석된 값 (`#FFFFFF`, `1.6rem`). |
|
| 6 | 6 |
* |
| 7 |
- * 선언 원문이 아니라 계산값을 쓴다 — Lightning CSS가 `light-dark()`를 |
|
| 7 |
+ * 선언 원문이 아니라 계산값을 쓴다 — 색상은 Lightning CSS가 `light-dark()`를 |
|
| 8 | 8 |
* `var(--lightningcss-light, …) var(--lightningcss-dark, …)`로 폴리필해 내보내므로 |
| 9 |
- * 원문은 읽기 어렵고, 계산값은 지금 테마에서 눈에 보이는 색과 정확히 일치한다. |
|
| 9 |
+ * 원문이 읽기 어렵고, 크기는 `alias`가 `var(--fox-…)` 참조라 원문만으로는 값을 알 수 |
|
| 10 |
+ * 없다. 계산값은 지금 화면에 실제로 적용되는 값과 정확히 일치한다. |
|
| 10 | 11 |
*/ |
| 11 | 12 |
value: string; |
| 12 | 13 |
/** 스타일에서 참조할 커스텀 프로퍼티 전체 이름 */ |
... | ... | @@ -16,13 +17,12 @@ |
| 16 | 17 |
export type TokenRegistry = Record<string, TokenEntry[]>; |
| 17 | 18 |
|
| 18 | 19 |
const PREFIX = "--fox-"; |
| 19 |
-const TYPO_FIELDS = ["-size", "-weight", "-line-height", "-letter-spacing"]; |
|
| 20 | 20 |
|
| 21 | 21 |
/** |
| 22 | 22 |
* 문서의 모든 스타일시트를 훑어 `--fox-*` 커스텀 프로퍼티를 그룹별로 모은다. |
| 23 | 23 |
* |
| 24 |
- * 토큰 목록을 카탈로그가 따로 들고 있지 않게 하려는 것이 목적이다 — `tokens/`에 값을 |
|
| 25 |
- * 추가하면 카탈로그에 자동으로 나타나고, 목록이 두 곳에 존재해 어긋날 일이 없다. |
|
| 24 |
+ * 토큰 목록을 카탈로그가 따로 들고 있지 않게 하려는 것이 목적이다 — `@fox/styles/tokens/`에 |
|
| 25 |
+ * 값을 추가하면 카탈로그에 자동으로 나타나고, 목록이 두 곳에 존재해 어긋날 일이 없다. |
|
| 26 | 26 |
* CSSOM은 선언 순서를 보존하므로 SCSS map에 적은 순서가 그대로 유지된다. |
| 27 | 27 |
* |
| 28 | 28 |
* 브라우저에서만 동작한다(문서가 있어야 한다). |
... | ... | @@ -59,11 +59,8 @@ |
| 59 | 59 |
continue; |
| 60 | 60 |
} |
| 61 | 61 |
|
| 62 |
- const group = rest.slice(0, separator); |
|
| 63 |
- const name = rest.slice(separator + 1); |
|
| 64 |
- |
|
| 65 |
- (registry[group] ??= []).push({
|
|
| 66 |
- name, |
|
| 62 |
+ (registry[rest.slice(0, separator)] ??= []).push({
|
|
| 63 |
+ name: rest.slice(separator + 1), |
|
| 67 | 64 |
value: |
| 68 | 65 |
computed.getPropertyValue(property).trim() || |
| 69 | 66 |
rule.style.getPropertyValue(property).trim(), |
... | ... | @@ -74,26 +71,4 @@ |
| 74 | 71 |
} |
| 75 | 72 |
|
| 76 | 73 |
return registry; |
| 77 |
-} |
|
| 78 |
- |
|
| 79 |
-/** |
|
| 80 |
- * 타이포 프리셋은 이름 하나가 4개의 커스텀 프로퍼티로 펼쳐져 있으므로 |
|
| 81 |
- * (`--fox-typo-title-md-size` 등) 접미사를 떼어 프리셋 이름만 순서대로 복원한다. |
|
| 82 |
- */ |
|
| 83 |
-export function typoPresetNames(registry: TokenRegistry): string[] {
|
|
| 84 |
- const names: string[] = []; |
|
| 85 |
- |
|
| 86 |
- for (const entry of registry.typo ?? []) {
|
|
| 87 |
- const field = TYPO_FIELDS.find((suffix) => entry.name.endsWith(suffix)); |
|
| 88 |
- if (!field) {
|
|
| 89 |
- continue; |
|
| 90 |
- } |
|
| 91 |
- |
|
| 92 |
- const preset = entry.name.slice(0, -field.length); |
|
| 93 |
- if (!names.includes(preset)) {
|
|
| 94 |
- names.push(preset); |
|
| 95 |
- } |
|
| 96 |
- } |
|
| 97 |
- |
|
| 98 |
- return names; |
|
| 99 | 74 |
} |
--- app/design/design.module.scss
+++ app/design/design.module.scss
... | ... | @@ -4,208 +4,158 @@ |
| 4 | 4 |
// SCSS 값을 JS로 내보내는 CSS Modules의 `:export`를 처음 썼으나, Next.js 16의 기본 |
| 5 | 5 |
// 번들러인 Turbopack은 Lightning CSS로 CSS를 파싱해 `:export`(webpack css-loader 시절의 |
| 6 | 6 |
// ICSS 확장)를 인식하지 못한다 — 빌드는 통과하지만 규칙이 통째로 버려져 값이 `undefined`가 |
| 7 |
-// 된다. `--fox-*` 커스텀 프로퍼티를 CSSOM에서 직접 열거하면 같은 "목록을 두 곳에 두지 |
|
| 8 |
-// 않는다"는 목적을 번들러 의존 없이 달성한다. |
|
| 7 |
+// 된다. `--fox-*` 커스텀 프로퍼티를 CSSOM에서 직접 열거하면 같은 목적을 번들러 의존 없이 |
|
| 8 |
+// 달성한다. |
|
| 9 | 9 |
|
| 10 | 10 |
@use "@fox/styles/abstracts" as fox; |
| 11 | 11 |
|
| 12 | 12 |
.page {
|
| 13 | 13 |
flex: 1; |
| 14 |
- padding: fox.space(8) fox.space(6); |
|
| 15 |
- |
|
| 16 |
- @include fox.container(xl, 6); |
|
| 14 |
+ padding: fox.spacing(top-lg) fox.grid(margin-default) fox.spacing(bottom-xlg); |
|
| 17 | 15 |
} |
| 18 | 16 |
|
| 19 | 17 |
.header {
|
| 20 | 18 |
display: flex; |
| 21 | 19 |
flex-wrap: wrap; |
| 22 |
- gap: fox.space(4); |
|
| 20 |
+ gap: fox.gap(5); |
|
| 23 | 21 |
align-items: baseline; |
| 24 | 22 |
justify-content: space-between; |
| 25 |
- padding-block-end: fox.space(6); |
|
| 26 |
- border-block-end: 1px solid fox.color(border); |
|
| 23 |
+ padding-block-end: fox.padding(7); |
|
| 24 |
+ border-block-end: fox.border(1) solid fox.color(border-neutral-default); |
|
| 27 | 25 |
} |
| 28 | 26 |
|
| 29 | 27 |
.title {
|
| 30 |
- @include fox.typo(headline-lg); |
|
| 28 |
+ font-size: fox.font-size(heading-lg); |
|
| 29 |
+ font-weight: fox.font-weight(bold); |
|
| 31 | 30 |
} |
| 32 | 31 |
|
| 33 | 32 |
.lede {
|
| 34 |
- margin-block-start: fox.space(2); |
|
| 35 |
- color: fox.color(foreground-muted); |
|
| 36 |
- |
|
| 37 |
- @include fox.typo(body-md); |
|
| 33 |
+ margin-block-start: fox.gap(2); |
|
| 34 |
+ color: fox.color(font-neutral-subtle); |
|
| 35 |
+ font-size: fox.font-size(body-md); |
|
| 38 | 36 |
} |
| 39 | 37 |
|
| 40 | 38 |
// ── 테마 전환 ─────────────────────────────────────────────────────────────── |
| 41 | 39 |
.themeSwitch {
|
| 42 | 40 |
display: flex; |
| 43 |
- gap: fox.space(1); |
|
| 44 |
- padding: fox.space(1); |
|
| 45 |
- border: 1px solid fox.color(border); |
|
| 46 |
- border-radius: fox.radius(full); |
|
| 47 |
- background: fox.color(surface-muted); |
|
| 41 |
+ gap: fox.gap(1); |
|
| 42 |
+ padding: fox.padding(1); |
|
| 43 |
+ border: fox.border(1) solid fox.color(border-neutral-default); |
|
| 44 |
+ border-radius: fox.radius(max); |
|
| 45 |
+ background: fox.color(surface-neutral-gray); |
|
| 48 | 46 |
} |
| 49 | 47 |
|
| 50 | 48 |
.themeButton {
|
| 51 |
- padding: fox.space(2) fox.space(4); |
|
| 52 |
- border-radius: fox.radius(full); |
|
| 53 |
- color: fox.color(foreground-muted); |
|
| 54 |
- |
|
| 55 |
- @include fox.typo(label-md); |
|
| 56 |
- @include fox.focus-ring; |
|
| 57 |
- @include fox.transition((background-color, color)); |
|
| 49 |
+ padding: fox.padding(3) fox.padding(6); |
|
| 50 |
+ border-radius: fox.radius(max); |
|
| 51 |
+ color: fox.color(font-neutral-subtle); |
|
| 52 |
+ font-size: fox.font-size(label-sm); |
|
| 53 |
+ font-weight: fox.font-weight(medium); |
|
| 58 | 54 |
|
| 59 | 55 |
&[aria-pressed="true"] {
|
| 60 |
- background: fox.color(primary); |
|
| 61 |
- color: fox.color(on-primary); |
|
| 56 |
+ background: fox.color(button-primary-surface); |
|
| 57 |
+ color: fox.color(button-primary-font); |
|
| 58 |
+ } |
|
| 59 |
+ |
|
| 60 |
+ &:focus-visible {
|
|
| 61 |
+ outline: fox.border(2) solid fox.color(border-theme-primary); |
|
| 62 |
+ outline-offset: fox.border(2); |
|
| 62 | 63 |
} |
| 63 | 64 |
} |
| 64 | 65 |
|
| 66 |
+// ── 섹션 ──────────────────────────────────────────────────────────────────── |
|
| 65 | 67 |
.section {
|
| 66 |
- padding-block: fox.space(8); |
|
| 67 |
- border-block-end: 1px solid fox.color(border-subtle); |
|
| 68 |
+ padding-block: fox.section(spacing-xsm); |
|
| 69 |
+ border-block-end: fox.border(1) solid fox.color(border-neutral-subtle); |
|
| 68 | 70 |
} |
| 69 | 71 |
|
| 70 | 72 |
.sectionTitle {
|
| 71 |
- margin-block-end: fox.space(1); |
|
| 72 |
- |
|
| 73 |
- @include fox.typo(title-lg); |
|
| 73 |
+ margin-block-end: fox.gap(1); |
|
| 74 |
+ font-size: fox.font-size(heading-sm); |
|
| 75 |
+ font-weight: fox.font-weight(bold); |
|
| 74 | 76 |
} |
| 75 | 77 |
|
| 76 | 78 |
.sectionNote {
|
| 77 |
- margin-block-end: fox.space(5); |
|
| 78 |
- color: fox.color(foreground-muted); |
|
| 79 |
- |
|
| 80 |
- @include fox.typo(body-sm); |
|
| 79 |
+ margin-block-end: fox.gap(5); |
|
| 80 |
+ color: fox.color(font-neutral-subtle); |
|
| 81 |
+ font-size: fox.font-size(body-sm); |
|
| 81 | 82 |
} |
| 82 | 83 |
|
| 83 | 84 |
.grid {
|
| 84 | 85 |
display: grid; |
| 85 |
- grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr)); |
|
| 86 |
- gap: fox.space(4); |
|
| 86 |
+ grid-template-columns: repeat(auto-fill, minmax(fox.grid(wrap-xsm), 1fr)); |
|
| 87 |
+ gap: fox.gap(4); |
|
| 88 |
+ |
|
| 89 |
+ @include fox.pc {
|
|
| 90 |
+ grid-template-columns: repeat(auto-fill, minmax(24rem, 1fr)); |
|
| 91 |
+ } |
|
| 87 | 92 |
} |
| 88 | 93 |
|
| 89 |
-// ── 색상 ──────────────────────────────────────────────────────────────────── |
|
| 94 |
+// ── 색상 스와치 ───────────────────────────────────────────────────────────── |
|
| 90 | 95 |
.swatch {
|
| 91 | 96 |
overflow: hidden; |
| 92 |
- border: 1px solid fox.color(border); |
|
| 93 |
- border-radius: fox.radius(md); |
|
| 94 |
- background: fox.color(surface); |
|
| 97 |
+ border: fox.border(1) solid fox.color(border-neutral-subtle); |
|
| 98 |
+ border-radius: fox.radius(3); |
|
| 99 |
+ background: fox.color(surface-neutral-default); |
|
| 95 | 100 |
} |
| 96 | 101 |
|
| 97 | 102 |
.swatchChip {
|
| 98 |
- block-size: 6.4rem; |
|
| 103 |
+ block-size: fox.icon(7); |
|
| 104 |
+ border-block-end: fox.border(1) solid fox.color(border-neutral-overlay); |
|
| 99 | 105 |
} |
| 100 | 106 |
|
| 101 | 107 |
.swatchMeta {
|
| 102 |
- padding: fox.space(3); |
|
| 108 |
+ padding: fox.padding(5); |
|
| 103 | 109 |
} |
| 104 | 110 |
|
| 105 | 111 |
.name {
|
| 106 | 112 |
display: block; |
| 107 |
- color: fox.color(foreground); |
|
| 113 |
+ color: fox.color(font-neutral-default); |
|
| 114 |
+ font-size: fox.font-size(label-sm); |
|
| 115 |
+ font-weight: fox.font-weight(medium); |
|
| 108 | 116 |
word-break: break-all; |
| 109 |
- |
|
| 110 |
- @include fox.typo(label-lg); |
|
| 111 | 117 |
} |
| 112 | 118 |
|
| 113 | 119 |
.ref {
|
| 114 | 120 |
display: block; |
| 115 |
- margin-block-start: fox.space(1); |
|
| 116 |
- color: fox.color(foreground-subtle); |
|
| 117 |
- font-family: fox.font(mono); |
|
| 121 |
+ margin-block-start: fox.gap(1); |
|
| 122 |
+ color: fox.color(font-neutral-subtler); |
|
| 123 |
+ font-size: fox.font-size(body-xsm); |
|
| 118 | 124 |
word-break: break-all; |
| 119 |
- |
|
| 120 |
- @include fox.typo(body-sm); |
|
| 121 |
-} |
|
| 122 |
- |
|
| 123 |
-// ── 타이포 ────────────────────────────────────────────────────────────────── |
|
| 124 |
-.typoRow {
|
|
| 125 |
- display: flex; |
|
| 126 |
- flex-wrap: wrap; |
|
| 127 |
- gap: fox.space(2) fox.space(6); |
|
| 128 |
- align-items: baseline; |
|
| 129 |
- padding-block: fox.space(3); |
|
| 130 |
- border-block-end: 1px solid fox.color(border-subtle); |
|
| 131 |
-} |
|
| 132 |
- |
|
| 133 |
-.typoName {
|
|
| 134 |
- flex: 0 0 14rem; |
|
| 135 |
- color: fox.color(foreground-subtle); |
|
| 136 |
- font-family: fox.font(mono); |
|
| 137 |
- |
|
| 138 |
- @include fox.typo(body-sm); |
|
| 139 |
-} |
|
| 140 |
- |
|
| 141 |
-.typoSample {
|
|
| 142 |
- flex: 1; |
|
| 143 |
- min-inline-size: 20rem; |
|
| 144 |
-} |
|
| 145 |
- |
|
| 146 |
-// ── 간격 ──────────────────────────────────────────────────────────────────── |
|
| 147 |
-.spaceRow {
|
|
| 148 |
- display: flex; |
|
| 149 |
- gap: fox.space(4); |
|
| 150 |
- align-items: center; |
|
| 151 |
- padding-block: fox.space(2); |
|
| 152 |
-} |
|
| 153 |
- |
|
| 154 |
-.spaceBar {
|
|
| 155 |
- block-size: 1.6rem; |
|
| 156 |
- border-radius: fox.radius(xs); |
|
| 157 |
- background: fox.color(primary); |
|
| 158 |
-} |
|
| 159 |
- |
|
| 160 |
-// ── radius · shadow ───────────────────────────────────────────────────────── |
|
| 161 |
-.tile {
|
|
| 162 |
- display: flex; |
|
| 163 |
- block-size: 9.6rem; |
|
| 164 |
- align-items: center; |
|
| 165 |
- justify-content: center; |
|
| 166 |
- border: 1px solid fox.color(border); |
|
| 167 |
- background: fox.color(surface); |
|
| 168 |
- color: fox.color(foreground-muted); |
|
| 169 |
- |
|
| 170 |
- @include fox.typo(label-md); |
|
| 171 |
-} |
|
| 172 |
- |
|
| 173 |
-.shadowTile {
|
|
| 174 |
- display: flex; |
|
| 175 |
- block-size: 9.6rem; |
|
| 176 |
- align-items: center; |
|
| 177 |
- justify-content: center; |
|
| 178 |
- border-radius: fox.radius(lg); |
|
| 179 |
- background: fox.color(surface); |
|
| 180 |
- color: fox.color(foreground-muted); |
|
| 181 |
- |
|
| 182 |
- @include fox.typo(label-md); |
|
| 183 |
-} |
|
| 184 |
- |
|
| 185 |
-.shadowCell {
|
|
| 186 |
- padding: fox.space(4); |
|
| 187 | 125 |
} |
| 188 | 126 |
|
| 189 | 127 |
// ── 값 테이블 ─────────────────────────────────────────────────────────────── |
| 190 | 128 |
.table {
|
| 191 | 129 |
inline-size: 100%; |
| 130 |
+ border-collapse: collapse; |
|
| 192 | 131 |
text-align: start; |
| 193 | 132 |
} |
| 194 | 133 |
|
| 195 | 134 |
.table th, |
| 196 | 135 |
.table td {
|
| 197 |
- padding: fox.space(2) fox.space(3); |
|
| 198 |
- border-block-end: 1px solid fox.color(border-subtle); |
|
| 136 |
+ padding: fox.padding(3) fox.padding(5); |
|
| 137 |
+ border-block-end: fox.border(1) solid fox.color(border-neutral-subtle); |
|
| 199 | 138 |
} |
| 200 | 139 |
|
| 201 | 140 |
.table th {
|
| 202 |
- color: fox.color(foreground-muted); |
|
| 203 |
- |
|
| 204 |
- @include fox.typo(label-md); |
|
| 141 |
+ color: fox.color(font-neutral-subtle); |
|
| 142 |
+ font-size: fox.font-size(label-xsm); |
|
| 143 |
+ font-weight: fox.font-weight(medium); |
|
| 205 | 144 |
} |
| 206 | 145 |
|
| 207 | 146 |
.table td {
|
| 208 |
- font-family: fox.font(mono); |
|
| 147 |
+ color: fox.color(font-neutral-default); |
|
| 148 |
+ font-size: fox.font-size(body-sm); |
|
| 149 |
+} |
|
| 209 | 150 |
|
| 210 |
- @include fox.typo(body-sm); |
|
| 151 |
+.tdValue {
|
|
| 152 |
+ color: fox.color(font-neutral-subtle); |
|
| 153 |
+} |
|
| 154 |
+ |
|
| 155 |
+// 크기 토큰을 눈으로 비교하기 위한 막대. 폭은 인라인 style이 토큰을 참조해 넣는다. |
|
| 156 |
+.bar {
|
|
| 157 |
+ display: block; |
|
| 158 |
+ block-size: fox.icon(2); |
|
| 159 |
+ border-radius: fox.radius(1); |
|
| 160 |
+ background: fox.color(surface-theme-primary); |
|
| 211 | 161 |
} |
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?