refactor: 디자인 카탈로그를 새 토큰 체계로 재작성
18개 그룹 531개를 CSSOM에서 읽어 그린다 — 목록을 코드에 두지 않으므로 토큰을 추가하면 카탈로그에 자동으로 나타난다. 색상은 스와치, 길이는 막대, 나머지는 값으로 표시한다. Co-Authored-By: Claude Opus 5
@f1924f2cc7b67729eadc17ce06584729d93c783b
--- 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?