feat: Badge 컴포넌트 3종 추가
Figma badge(391:4804) · badge-number(392:4810) · badge-dot(392:4816) · badge-group(392:4848)을 FoxBadge / FoxBadgeDot / FoxBadgeGroup으로 옮겼다. badge-number와 badge-dot을 합친 이유는 둘 다 알림 표시이고 색 축(primary·new)이 같으며 개수를 아느냐 모르느냐만 다르기 때문이다. count를 주면 숫자, 없으면 점이다. 색 9종 × 계열 3종 × 크기 3종은 색마다 쓰는 토큰이 규칙적이지 않아 SCSS 맵에 모아 한 번에 펼친다 — 손으로 27벌을 적으면 어긋난다. theme 계열은 solid에도 `-strong`을 쓰고, system 계열은 테두리가 `-strong`·solid가 기본값이며, warning만 solid 글자가 검정이고, disabled는 pastel도 solid와 같은 색이다(모두 시안 확인). 테두리를 border가 아니라 안쪽 그림자로 그린다. 시안에서 테두리가 크기를 늘리지 않아 outline과 solid의 높이가 같은데, CSS border는 상자를 키워 sm이 20px 대신 21px이 된다. 굵기는 그대로 border/1 토큰을 쓴다. FoxBadgeGroup의 size는 자식 배지를 덮어쓴다(FoxButtonGroup과 같은 방식). 승인된 비토큰 값: 최소 높이 2.4rem(md)·2rem(sm), 점 0.6rem, 숫자 배지 2rem — 모두 시안 값이나 Figma가 변수로 export하지 않는다. 검증: 색·계열 17조합의 바탕·글자·테두리 색이 시안 토큰과 정확히 일치. 크기 3종의 높이 31/24/20과 글자 15/13/11이 일치하며 계열이 달라도 높이가 같다. 숫자 배지는 자릿수가 늘어도 높이가 20으로 고정된다. sass 498규칙 · tsc · eslint 통과. Co-Authored-By: Claude Opus 5
@3f0f94d9004c95dd7611cc6363f28caa778e608c
+++ @fox/core/components/fox-badge-dot/fox-badge-dot.tsx
... | ... | @@ -0,0 +1,67 @@ |
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import { cx } from "../../utils"; | |
| 4 | + | |
| 5 | +export type FoxBadgeDotColor = "primary" | "new"; | |
| 6 | + | |
| 7 | +export interface FoxBadgeDotProps { | |
| 8 | + color?: FoxBadgeDotColor; | |
| 9 | + /** | |
| 10 | + * 개수. 주면 숫자 배지가 되고, 없으면 점 하나로 나온다 — 시안의 badge-number와 | |
| 11 | + * badge-dot이다. | |
| 12 | + */ | |
| 13 | + count?: number; | |
| 14 | + /** 넘으면 `99+`처럼 적는다. `count`가 없으면 쓰이지 않는다. */ | |
| 15 | + max?: number; | |
| 16 | + /** | |
| 17 | + * 스크린리더가 읽을 말. 점만 있을 때는 눈에 보이는 글자가 없어 이걸로 알린다. | |
| 18 | + * 옆 요소가 이미 알리고 있으면 빈 문자열을 줘서 잠재운다. | |
| 19 | + */ | |
| 20 | + label?: string; | |
| 21 | + /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */ | |
| 22 | + hidden?: boolean; | |
| 23 | + /** 배치 조정용. */ | |
| 24 | + className?: string; | |
| 25 | +} | |
| 26 | + | |
| 27 | +/** | |
| 28 | + * @fox 알림 배지. 개수를 주면 숫자로, 안 주면 점으로 나온다. | |
| 29 | + * | |
| 30 | + * 시안의 badge-number와 badge-dot을 하나로 둔다 — 둘 다 알림 표시이고 색 축(primary·new)이 | |
| 31 | + * 같으며, 개수를 아느냐 모르느냐만 다르다. | |
| 32 | + * | |
| 33 | + * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"` | |
| 34 | + * (또는 개별 파티셜)로 한 번 불러와야 한다. | |
| 35 | + */ | |
| 36 | +export function FoxBadgeDot({ | |
| 37 | + color = "primary", | |
| 38 | + count, | |
| 39 | + max, | |
| 40 | + label, | |
| 41 | + hidden = false, | |
| 42 | + className, | |
| 43 | +}: FoxBadgeDotProps) { | |
| 44 | + if (hidden) { | |
| 45 | + return null; | |
| 46 | + } | |
| 47 | + | |
| 48 | + const counted = count !== undefined; | |
| 49 | + const text = counted && max !== undefined && count > max ? `${max}+` : `${count}`; | |
| 50 | + const spoken = label ?? (counted ? `새 알림 ${text}건` : "새 알림"); | |
| 51 | + | |
| 52 | + return ( | |
| 53 | + <span | |
| 54 | + className={cx( | |
| 55 | + "fox-badge-dot", | |
| 56 | + `fox-badge-dot--${color}`, | |
| 57 | + counted && "fox-badge-dot--counted", | |
| 58 | + className | |
| 59 | + )} | |
| 60 | + role={spoken ? "status" : undefined} | |
| 61 | + aria-label={spoken || undefined} | |
| 62 | + > | |
| 63 | + {/* 보이는 숫자는 읽히지 않게 한다 — 위 `aria-label`이 이미 같은 말을 한다. */} | |
| 64 | + {counted && <span aria-hidden="true">{text}</span>} | |
| 65 | + </span> | |
| 66 | + ); | |
| 67 | +} |
+++ @fox/core/components/fox-badge-dot/index.ts
... | ... | @@ -0,0 +1,5 @@ |
| 1 | +export { | |
| 2 | + FoxBadgeDot, | |
| 3 | + type FoxBadgeDotProps, | |
| 4 | + type FoxBadgeDotColor, | |
| 5 | +} from "./fox-badge-dot"; |
+++ @fox/core/components/fox-badge-group/fox-badge-group.tsx
... | ... | @@ -0,0 +1,65 @@ |
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import { | |
| 4 | + Children, | |
| 5 | + Fragment, | |
| 6 | + cloneElement, | |
| 7 | + isValidElement, | |
| 8 | + type ReactElement, | |
| 9 | + type ReactNode, | |
| 10 | +} from "react"; | |
| 11 | +import { cx } from "../../utils"; | |
| 12 | +import type { FoxBadgeSize } from "../fox-badge"; | |
| 13 | + | |
| 14 | +export interface FoxBadgeGroupProps { | |
| 15 | + children?: ReactNode; | |
| 16 | + /** 자식 배지의 `size`를 전부 이 값으로 덮어쓴다. 자식이 따로 준 값은 무시된다. */ | |
| 17 | + size?: FoxBadgeSize; | |
| 18 | + /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */ | |
| 19 | + hidden?: boolean; | |
| 20 | + /** 배치 조정용. */ | |
| 21 | + className?: string; | |
| 22 | +} | |
| 23 | + | |
| 24 | +/** | |
| 25 | + * 자식 배지에 그룹 크기를 강제로 덮어쓴다. FoxButtonGroup과 같은 사정이라 같은 방식이다 — | |
| 26 | + * 호스트 요소는 건드리지 않고, `Children.map`이 들어가지 않는 프래그먼트는 직접 내려간다. | |
| 27 | + */ | |
| 28 | +function overrideChildren(children: ReactNode, size: FoxBadgeSize): ReactNode { | |
| 29 | + return Children.map(children, (child) => { | |
| 30 | + if (!isValidElement(child)) { | |
| 31 | + return child; | |
| 32 | + } | |
| 33 | + | |
| 34 | + if (child.type === Fragment) { | |
| 35 | + const fragment = child as ReactElement<{ children?: ReactNode }>; | |
| 36 | + return overrideChildren(fragment.props.children, size); | |
| 37 | + } | |
| 38 | + | |
| 39 | + if (typeof child.type === "string") { | |
| 40 | + return child; | |
| 41 | + } | |
| 42 | + | |
| 43 | + return cloneElement(child as ReactElement<Record<string, unknown>>, { size }); | |
| 44 | + }); | |
| 45 | +} | |
| 46 | + | |
| 47 | +/** | |
| 48 | + * @fox 배지 묶음. 배지를 가로로 늘어놓고 폭이 모자라면 줄을 넘긴다. | |
| 49 | + * | |
| 50 | + * `size`를 주면 안의 배지 크기가 그 값으로 통일된다. 주지 않으면 배지가 각자 정한다. | |
| 51 | + * | |
| 52 | + * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"` | |
| 53 | + * (또는 개별 파티셜)로 한 번 불러와야 한다. | |
| 54 | + */ | |
| 55 | +export function FoxBadgeGroup({ children, size, hidden = false, className }: FoxBadgeGroupProps) { | |
| 56 | + if (hidden) { | |
| 57 | + return null; | |
| 58 | + } | |
| 59 | + | |
| 60 | + return ( | |
| 61 | + <div className={cx("fox-badge-group", className)}> | |
| 62 | + {size ? overrideChildren(children, size) : children} | |
| 63 | + </div> | |
| 64 | + ); | |
| 65 | +} |
+++ @fox/core/components/fox-badge-group/index.ts
... | ... | @@ -0,0 +1,1 @@ |
| 1 | +export { FoxBadgeGroup, type FoxBadgeGroupProps } from "./fox-badge-group"; |
+++ @fox/core/components/fox-badge/fox-badge.tsx
... | ... | @@ -0,0 +1,65 @@ |
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import type { ReactNode } from "react"; | |
| 4 | +import { cx } from "../../utils"; | |
| 5 | + | |
| 6 | +export type FoxBadgeColor = | |
| 7 | + | "neutral" | |
| 8 | + | "primary" | |
| 9 | + | "secondary" | |
| 10 | + | "accent" | |
| 11 | + | "danger" | |
| 12 | + | "warning" | |
| 13 | + | "success" | |
| 14 | + | "information" | |
| 15 | + | "disabled"; | |
| 16 | +export type FoxBadgeType = "outline" | "solid" | "pastel"; | |
| 17 | +export type FoxBadgeSize = "lg" | "md" | "sm"; | |
| 18 | + | |
| 19 | +export interface FoxBadgeProps { | |
| 20 | + children: ReactNode; | |
| 21 | + color?: FoxBadgeColor; | |
| 22 | + /** `outline`은 테두리만, `solid`는 색을 채우고, `pastel`은 옅게 깐다. */ | |
| 23 | + type?: FoxBadgeType; | |
| 24 | + size?: FoxBadgeSize; | |
| 25 | + /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */ | |
| 26 | + hidden?: boolean; | |
| 27 | + /** 배치 조정용. 모양이 달라야 하면 여기 말고 `color`·`type`을 쓴다. */ | |
| 28 | + className?: string; | |
| 29 | +} | |
| 30 | + | |
| 31 | +/** | |
| 32 | + * @fox 배지. 상태를 갖지 않는 표시용 조각이라 누를 수 없다. | |
| 33 | + * | |
| 34 | + * 색 9종 × 계열 3종 × 크기 3종이 모두 시안에 있다. `disabled`는 잠긴 상태가 아니라 | |
| 35 | + * 색 이름이다 — 배지는 누를 것이 없어 상태가 없고, 시안도 색 축에 두었다. | |
| 36 | + * | |
| 37 | + * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"` | |
| 38 | + * (또는 개별 파티셜)로 한 번 불러와야 한다. | |
| 39 | + */ | |
| 40 | +export function FoxBadge({ | |
| 41 | + children, | |
| 42 | + color = "neutral", | |
| 43 | + type = "outline", | |
| 44 | + size = "lg", | |
| 45 | + hidden = false, | |
| 46 | + className, | |
| 47 | +}: FoxBadgeProps) { | |
| 48 | + if (hidden) { | |
| 49 | + return null; | |
| 50 | + } | |
| 51 | + | |
| 52 | + return ( | |
| 53 | + <span | |
| 54 | + className={cx( | |
| 55 | + "fox-badge", | |
| 56 | + `fox-badge--${color}`, | |
| 57 | + `fox-badge--${type}`, | |
| 58 | + `fox-badge--${size}`, | |
| 59 | + className | |
| 60 | + )} | |
| 61 | + > | |
| 62 | + {children} | |
| 63 | + </span> | |
| 64 | + ); | |
| 65 | +} |
+++ @fox/core/components/fox-badge/index.ts
... | ... | @@ -0,0 +1,7 @@ |
| 1 | +export { | |
| 2 | + FoxBadge, | |
| 3 | + type FoxBadgeProps, | |
| 4 | + type FoxBadgeColor, | |
| 5 | + type FoxBadgeType, | |
| 6 | + type FoxBadgeSize, | |
| 7 | +} from "./fox-badge"; |
--- @fox/core/components/index.ts
+++ @fox/core/components/index.ts
... | ... | @@ -1,4 +1,7 @@ |
| 1 | 1 |
export * from "./fox-address"; |
| 2 |
+export * from "./fox-badge"; |
|
| 3 |
+export * from "./fox-badge-dot"; |
|
| 4 |
+export * from "./fox-badge-group"; |
|
| 2 | 5 |
export * from "./fox-breadcrumb"; |
| 3 | 6 |
export * from "./fox-bullet-list"; |
| 4 | 7 |
export * from "./fox-bullet-list-item"; |
--- @fox/dev-test/component-registry.tsx
+++ @fox/dev-test/component-registry.tsx
... | ... | @@ -39,6 +39,9 @@ |
| 39 | 39 |
} from "../core/icons"; |
| 40 | 40 |
import { FoxBulletList } from "../core/components/fox-bullet-list";
|
| 41 | 41 |
import { FoxBulletListItem } from "../core/components/fox-bullet-list-item";
|
| 42 |
+import { FoxBadge } from "../core/components/fox-badge";
|
|
| 43 |
+import { FoxBadgeDot } from "../core/components/fox-badge-dot";
|
|
| 44 |
+import { FoxBadgeGroup } from "../core/components/fox-badge-group";
|
|
| 42 | 45 |
import styles from "./dev-test.module.scss"; |
| 43 | 46 |
|
| 44 | 47 |
export interface ComponentExample {
|
... | ... | @@ -435,6 +438,35 @@ |
| 435 | 438 |
disabled={filters.length === TAG_FILTERS.length}
|
| 436 | 439 |
/> |
| 437 | 440 |
</> |
| 441 |
+ ); |
|
| 442 |
+} |
|
| 443 |
+ |
|
| 444 |
+const BADGE_COLORS = [ |
|
| 445 |
+ "neutral", |
|
| 446 |
+ "primary", |
|
| 447 |
+ "secondary", |
|
| 448 |
+ "accent", |
|
| 449 |
+ "danger", |
|
| 450 |
+ "warning", |
|
| 451 |
+ "success", |
|
| 452 |
+ "information", |
|
| 453 |
+ "disabled", |
|
| 454 |
+] as const; |
|
| 455 |
+ |
|
| 456 |
+/** 시안의 9색 × 3계열 표를 그대로 펼친다 — 눈으로 비교해야 하는 조합이라 빠짐없이 낸다. */ |
|
| 457 |
+function BadgeMatrix({ size }: { size: "lg" | "md" | "sm" }) {
|
|
| 458 |
+ return ( |
|
| 459 |
+ <div className={styles.demoStack}>
|
|
| 460 |
+ {(["outline", "solid", "pastel"] as const).map((type) => (
|
|
| 461 |
+ <div className={styles.demoRow} key={type}>
|
|
| 462 |
+ {BADGE_COLORS.map((color) => (
|
|
| 463 |
+ <FoxBadge key={color} color={color} type={type} size={size}>
|
|
| 464 |
+ 레이블 |
|
| 465 |
+ </FoxBadge> |
|
| 466 |
+ ))} |
|
| 467 |
+ </div> |
|
| 468 |
+ ))} |
|
| 469 |
+ </div> |
|
| 438 | 470 |
); |
| 439 | 471 |
} |
| 440 | 472 |
|
... | ... | @@ -2383,4 +2415,134 @@ |
| 2383 | 2415 |
}, |
| 2384 | 2416 |
], |
| 2385 | 2417 |
}, |
| 2418 |
+ {
|
|
| 2419 |
+ id: "fox-badge", |
|
| 2420 |
+ name: "FoxBadge", |
|
| 2421 |
+ description: |
|
| 2422 |
+ "Figma 시안(badge 190:3054 · 섹션 391:4804)을 옮겼습니다. 색 9종 × 계열 3종 × 크기 3종이 모두 시안에 있습니다. disabled는 잠긴 상태가 아니라 색 이름입니다 — 배지는 누를 것이 없어 상태가 없고 시안도 색 축에 두었습니다. warning만 solid 글자가 검정인데 노란 바탕에 흰 글자가 읽히지 않아서입니다.", |
|
| 2423 |
+ variants: [ |
|
| 2424 |
+ { label: "lg — 계열 3종 × 색 9종", node: <BadgeMatrix size="lg" /> },
|
|
| 2425 |
+ { label: "md", node: <BadgeMatrix size="md" /> },
|
|
| 2426 |
+ { label: "sm", node: <BadgeMatrix size="sm" /> },
|
|
| 2427 |
+ {
|
|
| 2428 |
+ label: "글자가 길어질 때 — 폭은 내용을 따르고 줄바꿈은 하지 않습니다", |
|
| 2429 |
+ node: ( |
|
| 2430 |
+ <div className={styles.demoRow}>
|
|
| 2431 |
+ <FoxBadge color="primary" type="pastel"> |
|
| 2432 |
+ 아주 긴 배지 레이블입니다 |
|
| 2433 |
+ </FoxBadge> |
|
| 2434 |
+ <FoxBadge color="success" type="solid" size="sm"> |
|
| 2435 |
+ 짧게 |
|
| 2436 |
+ </FoxBadge> |
|
| 2437 |
+ </div> |
|
| 2438 |
+ ), |
|
| 2439 |
+ }, |
|
| 2440 |
+ ], |
|
| 2441 |
+ }, |
|
| 2442 |
+ {
|
|
| 2443 |
+ id: "fox-badge-dot", |
|
| 2444 |
+ name: "FoxBadgeDot", |
|
| 2445 |
+ description: |
|
| 2446 |
+ "Figma 시안의 badge-dot(190:3359)과 badge-number(190:3354)를 한 컴포넌트로 두었습니다. 둘 다 알림 표시이고 색 축이 같아서, 개수를 주면 숫자 배지가 되고 안 주면 점으로 나옵니다.", |
|
| 2447 |
+ variants: [ |
|
| 2448 |
+ {
|
|
| 2449 |
+ label: "점 — 개수를 모를 때", |
|
| 2450 |
+ node: ( |
|
| 2451 |
+ <div className={styles.demoRow}>
|
|
| 2452 |
+ <FoxBadgeDot /> |
|
| 2453 |
+ <FoxBadgeDot color="new" /> |
|
| 2454 |
+ </div> |
|
| 2455 |
+ ), |
|
| 2456 |
+ }, |
|
| 2457 |
+ {
|
|
| 2458 |
+ label: "숫자 — 개수를 알 때", |
|
| 2459 |
+ node: ( |
|
| 2460 |
+ <div className={styles.demoRow}>
|
|
| 2461 |
+ <FoxBadgeDot count={5} />
|
|
| 2462 |
+ <FoxBadgeDot color="new" count={5} />
|
|
| 2463 |
+ </div> |
|
| 2464 |
+ ), |
|
| 2465 |
+ }, |
|
| 2466 |
+ {
|
|
| 2467 |
+ label: "자릿수가 늘면 옆으로만 늘어납니다", |
|
| 2468 |
+ node: ( |
|
| 2469 |
+ <div className={styles.demoRow}>
|
|
| 2470 |
+ <FoxBadgeDot count={1} />
|
|
| 2471 |
+ <FoxBadgeDot count={12} />
|
|
| 2472 |
+ <FoxBadgeDot count={128} />
|
|
| 2473 |
+ <FoxBadgeDot count={1280} max={99} />
|
|
| 2474 |
+ </div> |
|
| 2475 |
+ ), |
|
| 2476 |
+ }, |
|
| 2477 |
+ ], |
|
| 2478 |
+ }, |
|
| 2479 |
+ {
|
|
| 2480 |
+ id: "fox-badge-group", |
|
| 2481 |
+ name: "FoxBadgeGroup", |
|
| 2482 |
+ description: |
|
| 2483 |
+ "Figma 시안(badge-group 190:3375)을 옮겼습니다. 배지를 가로로 늘어놓고 폭이 모자라면 줄을 넘깁니다. size를 주면 안의 배지 크기가 그 값으로 통일되고, 자식이 따로 준 size는 무효화됩니다(FoxButtonGroup과 같은 방식).", |
|
| 2484 |
+ variants: [ |
|
| 2485 |
+ {
|
|
| 2486 |
+ label: "크기 통일 (size) — 자식이 준 값은 무시됩니다", |
|
| 2487 |
+ node: ( |
|
| 2488 |
+ <div className={styles.demoStack}>
|
|
| 2489 |
+ <FoxBadgeGroup size="lg"> |
|
| 2490 |
+ <FoxBadge color="primary" type="pastel" size="sm"> |
|
| 2491 |
+ lg로 덮임 |
|
| 2492 |
+ </FoxBadge> |
|
| 2493 |
+ <FoxBadge color="success" type="pastel"> |
|
| 2494 |
+ lg로 덮임 |
|
| 2495 |
+ </FoxBadge> |
|
| 2496 |
+ </FoxBadgeGroup> |
|
| 2497 |
+ <FoxBadgeGroup size="md"> |
|
| 2498 |
+ <FoxBadge color="primary" type="pastel"> |
|
| 2499 |
+ md |
|
| 2500 |
+ </FoxBadge> |
|
| 2501 |
+ <FoxBadge color="success" type="pastel"> |
|
| 2502 |
+ md |
|
| 2503 |
+ </FoxBadge> |
|
| 2504 |
+ </FoxBadgeGroup> |
|
| 2505 |
+ <FoxBadgeGroup size="sm"> |
|
| 2506 |
+ <FoxBadge color="primary" type="pastel"> |
|
| 2507 |
+ sm |
|
| 2508 |
+ </FoxBadge> |
|
| 2509 |
+ <FoxBadge color="success" type="pastel"> |
|
| 2510 |
+ sm |
|
| 2511 |
+ </FoxBadge> |
|
| 2512 |
+ </FoxBadgeGroup> |
|
| 2513 |
+ </div> |
|
| 2514 |
+ ), |
|
| 2515 |
+ }, |
|
| 2516 |
+ {
|
|
| 2517 |
+ label: "size를 안 주면 배지가 각자 정합니다", |
|
| 2518 |
+ node: ( |
|
| 2519 |
+ <FoxBadgeGroup> |
|
| 2520 |
+ <FoxBadge color="primary" type="pastel" size="lg"> |
|
| 2521 |
+ lg |
|
| 2522 |
+ </FoxBadge> |
|
| 2523 |
+ <FoxBadge color="success" type="pastel" size="md"> |
|
| 2524 |
+ md |
|
| 2525 |
+ </FoxBadge> |
|
| 2526 |
+ <FoxBadge color="warning" type="pastel" size="sm"> |
|
| 2527 |
+ sm |
|
| 2528 |
+ </FoxBadge> |
|
| 2529 |
+ </FoxBadgeGroup> |
|
| 2530 |
+ ), |
|
| 2531 |
+ }, |
|
| 2532 |
+ {
|
|
| 2533 |
+ label: "좁아지면 줄이 넘어갑니다", |
|
| 2534 |
+ node: ( |
|
| 2535 |
+ <div className={styles.demoNarrow}>
|
|
| 2536 |
+ <FoxBadgeGroup size="md"> |
|
| 2537 |
+ {BADGE_COLORS.map((color) => (
|
|
| 2538 |
+ <FoxBadge key={color} color={color} type="pastel">
|
|
| 2539 |
+ {color}
|
|
| 2540 |
+ </FoxBadge> |
|
| 2541 |
+ ))} |
|
| 2542 |
+ </FoxBadgeGroup> |
|
| 2543 |
+ </div> |
|
| 2544 |
+ ), |
|
| 2545 |
+ }, |
|
| 2546 |
+ ], |
|
| 2547 |
+ }, |
|
| 2386 | 2548 |
]; |
--- @fox/dev-test/dev-test.module.scss
+++ @fox/dev-test/dev-test.module.scss
... | ... | @@ -488,3 +488,11 @@ |
| 488 | 488 |
border-radius: fox.radius(3); |
| 489 | 489 |
background: fox.color(surface-neutral-gray-stronger); |
| 490 | 490 |
} |
| 491 |
+ |
|
| 492 |
+// 조합을 한 줄에 늘어놓고 비교할 때. 좁아지면 줄이 넘어간다. |
|
| 493 |
+.demoRow {
|
|
| 494 |
+ display: flex; |
|
| 495 |
+ flex-wrap: wrap; |
|
| 496 |
+ align-items: center; |
|
| 497 |
+ gap: fox.gap(3); |
|
| 498 |
+} |
+++ @fox/styles/_fox-badge-dot.scss
... | ... | @@ -0,0 +1,53 @@ |
| 1 | +// FoxBadgeDot — 시안: 통합관리자페이지 디자인시스템(KE9UILWhB8qTBe14uXUn4F) | |
| 2 | +// badge-dot (190:3359 · 섹션 392:4816) | |
| 3 | +// badge-number (190:3354 · 섹션 392:4810) | |
| 4 | +// | |
| 5 | +// 마크업 계약 (React 밖 소비자용): | |
| 6 | +// <span class="fox-badge-dot fox-badge-dot--primary"></span> | |
| 7 | +// <span class="fox-badge-dot fox-badge-dot--primary fox-badge-dot--counted"><span>5</span></span> | |
| 8 | +// | |
| 9 | +// 두 시안이 한 컴포넌트다 — 둘 다 알림 표시이고 색 축(primary·new)이 같으며, 개수를 | |
| 10 | +// 아느냐 모르느냐만 다르다. 개수가 붙으면 점이 알약으로 늘어난다. | |
| 11 | +// | |
| 12 | +// 토큰이 아닌 값: | |
| 13 | +// - 점 0.6rem, 숫자 배지 높이·최소폭 2rem: 시안 값이나 Figma가 변수로 export하지 | |
| 14 | +// 않는다(사용자 승인). | |
| 15 | +// - line-height / letter-spacing: 시안 텍스트 스타일의 값이나 Figma가 변수로 | |
| 16 | +// export하지 않는다. | |
| 17 | + | |
| 18 | +@use "abstracts" as fox; | |
| 19 | + | |
| 20 | +.fox-badge-dot { | |
| 21 | + box-sizing: border-box; | |
| 22 | + display: inline-flex; | |
| 23 | + flex-shrink: 0; | |
| 24 | + align-items: center; | |
| 25 | + justify-content: center; | |
| 26 | + // 시안 값이나 변수로 export되지 않는다 — 파일 상단 참고. | |
| 27 | + inline-size: 0.6rem; | |
| 28 | + block-size: 0.6rem; | |
| 29 | + border-radius: fox.radius(max); | |
| 30 | + | |
| 31 | + // 개수가 붙으면 알약이 되고 글자만큼 옆으로 늘어난다. | |
| 32 | + &--counted { | |
| 33 | + inline-size: auto; | |
| 34 | + min-inline-size: 2rem; | |
| 35 | + block-size: 2rem; | |
| 36 | + padding-inline: fox.padding(2); | |
| 37 | + color: fox.color(font-neutral-static-inverse); | |
| 38 | + font-family: fox.font-family(body); | |
| 39 | + font-size: fox.font-size(label-sm); | |
| 40 | + font-weight: fox.font-weight(bold); | |
| 41 | + line-height: 1; | |
| 42 | + letter-spacing: -0.025em; | |
| 43 | + text-align: center; | |
| 44 | + } | |
| 45 | + | |
| 46 | + &--primary { | |
| 47 | + background: fox.color(element-theme-primary); | |
| 48 | + } | |
| 49 | + | |
| 50 | + &--new { | |
| 51 | + background: fox.color(element-theme-accent); | |
| 52 | + } | |
| 53 | +} |
+++ @fox/styles/_fox-badge-group.scss
... | ... | @@ -0,0 +1,17 @@ |
| 1 | +// FoxBadgeGroup — 시안: 통합관리자페이지 디자인시스템(KE9UILWhB8qTBe14uXUn4F) | |
| 2 | +// badge-group (190:3375 · 섹션 392:4848) | |
| 3 | +// | |
| 4 | +// 마크업 계약 (React 밖 소비자용): | |
| 5 | +// <div class="fox-badge-group">…fox-badge 여럿…</div> | |
| 6 | +// | |
| 7 | +// 시안에서 안에 든 것이 badge 인스턴스라 이 파일은 줄 배치만 갖는다. 시안의 size 축은 | |
| 8 | +// 그룹이 자식 배지에 크기를 덮어써서 이룬다(FoxButtonGroup과 같은 방식). | |
| 9 | + | |
| 10 | +@use "abstracts" as fox; | |
| 11 | + | |
| 12 | +.fox-badge-group { | |
| 13 | + display: flex; | |
| 14 | + flex-wrap: wrap; | |
| 15 | + align-items: flex-start; | |
| 16 | + gap: fox.gap(2); | |
| 17 | +} |
+++ @fox/styles/_fox-badge.scss
... | ... | @@ -0,0 +1,153 @@ |
| 1 | +// FoxBadge — 시안: 통합관리자페이지 디자인시스템(KE9UILWhB8qTBe14uXUn4F) | |
| 2 | +// badge (190:3054 · 섹션 391:4804) | |
| 3 | +// | |
| 4 | +// 마크업 계약 (React 밖 소비자용): | |
| 5 | +// <span class="fox-badge fox-badge--neutral fox-badge--outline fox-badge--lg">레이블</span> | |
| 6 | +// | |
| 7 | +// 색 9종 × 계열 3종(outline·solid·pastel) × 크기 3종이 모두 시안에 있다. 색마다 쓰는 토큰이 | |
| 8 | +// 규칙적이지 않아 아래 표에 모아 두고 한 번에 펼친다 — 손으로 27벌을 적으면 어긋난다. | |
| 9 | +// - theme 계열(primary·secondary·accent)은 solid에도 `-strong`을 쓴다(기본값이 없다). | |
| 10 | +// - system 계열(danger·warning·success·information)은 테두리가 `-strong`, solid가 기본값이다. | |
| 11 | +// - warning만 solid 글자가 검정이다 — 노란 바탕에 흰 글자는 읽히지 않는다. | |
| 12 | +// - disabled는 pastel도 solid와 같은 색이다(시안 확인). | |
| 13 | +// | |
| 14 | +// 테두리를 `border`가 아니라 안쪽 그림자로 그린다. 시안에서 테두리가 크기를 늘리지 않아 | |
| 15 | +// outline과 solid의 높이가 같은데, CSS `border`는 상자를 키워 1~2px씩 어긋나기 때문이다. | |
| 16 | +// 굵기는 그대로 `border/1` 토큰을 쓴다. | |
| 17 | +// | |
| 18 | +// 토큰이 아닌 값: | |
| 19 | +// - 최소 높이 2.4rem(md) · 2rem(sm): 시안 값이나 Figma가 변수로 export하지 않는다 | |
| 20 | +// (사용자 승인). lg는 여백과 글자만으로 시안 높이가 나와 따로 두지 않는다. | |
| 21 | +// - line-height / letter-spacing: 시안 텍스트 스타일의 값이나 Figma가 변수로 | |
| 22 | +// export하지 않는다. | |
| 23 | + | |
| 24 | +@use "sass:map"; | |
| 25 | +@use "abstracts" as fox; | |
| 26 | + | |
| 27 | +// line = outline 테두리 · text = outline·pastel 글자 · fill = solid 바탕 | |
| 28 | +// on-fill = solid 글자 · soft = pastel 바탕 · soft-text = pastel 글자(다를 때만) | |
| 29 | +$badge-colors: ( | |
| 30 | + neutral: ( | |
| 31 | + line: element-neutral-default, | |
| 32 | + text: font-neutral-default, | |
| 33 | + fill: element-neutral-default, | |
| 34 | + on-fill: font-neutral-static-inverse, | |
| 35 | + soft: element-neutral-subtle, | |
| 36 | + ), | |
| 37 | + primary: ( | |
| 38 | + line: element-theme-primary-strong, | |
| 39 | + text: font-theme-primary, | |
| 40 | + fill: element-theme-primary-strong, | |
| 41 | + on-fill: font-neutral-static-inverse, | |
| 42 | + soft: element-theme-primary-subtler, | |
| 43 | + ), | |
| 44 | + secondary: ( | |
| 45 | + line: element-theme-secondary-strong, | |
| 46 | + text: font-theme-secondary, | |
| 47 | + fill: element-theme-secondary-strong, | |
| 48 | + on-fill: font-neutral-static-inverse, | |
| 49 | + soft: element-theme-secondary-subtler, | |
| 50 | + ), | |
| 51 | + accent: ( | |
| 52 | + line: element-theme-accent-strong, | |
| 53 | + text: font-theme-accent, | |
| 54 | + fill: element-theme-accent-strong, | |
| 55 | + on-fill: font-neutral-static-inverse, | |
| 56 | + soft: element-theme-accent-subtler, | |
| 57 | + ), | |
| 58 | + danger: ( | |
| 59 | + line: element-system-danger-strong, | |
| 60 | + text: font-system-danger, | |
| 61 | + fill: element-system-danger, | |
| 62 | + on-fill: font-neutral-static-inverse, | |
| 63 | + soft: element-system-danger-inverse, | |
| 64 | + ), | |
| 65 | + warning: ( | |
| 66 | + line: element-system-warning-strong, | |
| 67 | + text: font-system-warning, | |
| 68 | + fill: element-system-warning, | |
| 69 | + on-fill: font-neutral-static, | |
| 70 | + soft: element-system-warning-inverse, | |
| 71 | + ), | |
| 72 | + success: ( | |
| 73 | + line: element-system-success-strong, | |
| 74 | + text: font-system-success, | |
| 75 | + fill: element-system-success, | |
| 76 | + on-fill: font-neutral-static-inverse, | |
| 77 | + soft: element-system-success-inverse, | |
| 78 | + ), | |
| 79 | + information: ( | |
| 80 | + line: element-system-information-strong, | |
| 81 | + text: font-system-information, | |
| 82 | + fill: element-system-information, | |
| 83 | + on-fill: font-neutral-static-inverse, | |
| 84 | + soft: element-system-information-inverse, | |
| 85 | + ), | |
| 86 | + disabled: ( | |
| 87 | + line: element-neutral-disabled, | |
| 88 | + text: font-neutral-disabled, | |
| 89 | + fill: element-neutral-disabled, | |
| 90 | + on-fill: font-neutral-disabled-strong, | |
| 91 | + soft: element-neutral-disabled, | |
| 92 | + soft-text: font-neutral-disabled-strong, | |
| 93 | + ), | |
| 94 | +); | |
| 95 | + | |
| 96 | +.fox-badge { | |
| 97 | + box-sizing: border-box; | |
| 98 | + display: inline-flex; | |
| 99 | + align-items: center; | |
| 100 | + justify-content: center; | |
| 101 | + border-radius: fox.radius(2); | |
| 102 | + font-family: fox.font-family(body); | |
| 103 | + font-weight: fox.font-weight(regular); | |
| 104 | + line-height: 1; | |
| 105 | + letter-spacing: -0.025em; | |
| 106 | + white-space: nowrap; | |
| 107 | + | |
| 108 | + // ── 크기 ────────────────────────────────────────────────────────────────── | |
| 109 | + &--lg { | |
| 110 | + padding: fox.padding(3); | |
| 111 | + font-size: fox.font-size(label-md); | |
| 112 | + } | |
| 113 | + | |
| 114 | + // 시안 값이나 변수로 export되지 않는다 — 파일 상단 참고. | |
| 115 | + &--md { | |
| 116 | + min-block-size: 2.4rem; | |
| 117 | + padding-inline: fox.padding(3); | |
| 118 | + padding-block: fox.padding(2); | |
| 119 | + font-size: fox.font-size(label-sm); | |
| 120 | + } | |
| 121 | + | |
| 122 | + &--sm { | |
| 123 | + min-block-size: 2rem; | |
| 124 | + padding: fox.padding(2); | |
| 125 | + font-size: fox.font-size(label-xsm); | |
| 126 | + } | |
| 127 | +} | |
| 128 | + | |
| 129 | +// ── 색 × 계열 ─────────────────────────────────────────────────────────────── | |
| 130 | +@each $name, $tokens in $badge-colors { | |
| 131 | + $soft-text: map.get($tokens, text); | |
| 132 | + | |
| 133 | + @if map.has-key($tokens, soft-text) { | |
| 134 | + $soft-text: map.get($tokens, soft-text); | |
| 135 | + } | |
| 136 | + | |
| 137 | + .fox-badge--#{$name} { | |
| 138 | + &.fox-badge--outline { | |
| 139 | + box-shadow: inset 0 0 0 fox.border(1) fox.color(map.get($tokens, line)); | |
| 140 | + color: fox.color(map.get($tokens, text)); | |
| 141 | + } | |
| 142 | + | |
| 143 | + &.fox-badge--solid { | |
| 144 | + background: fox.color(map.get($tokens, fill)); | |
| 145 | + color: fox.color(map.get($tokens, on-fill)); | |
| 146 | + } | |
| 147 | + | |
| 148 | + &.fox-badge--pastel { | |
| 149 | + background: fox.color(map.get($tokens, soft)); | |
| 150 | + color: fox.color($soft-text); | |
| 151 | + } | |
| 152 | + } | |
| 153 | +} |
--- @fox/styles/components.scss
+++ @fox/styles/components.scss
... | ... | @@ -35,3 +35,6 @@ |
| 35 | 35 |
@use "fox-toggle-switch"; |
| 36 | 36 |
@use "fox-bullet-list"; |
| 37 | 37 |
@use "fox-bullet-list-item"; |
| 38 |
+@use "fox-badge"; |
|
| 39 |
+@use "fox-badge-dot"; |
|
| 40 |
+@use "fox-badge-group"; |
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?