+++ @fox/core/components/fox-status-indicator/fox-status-indicator.tsx
... | ... | @@ -0,0 +1,71 @@ |
| 1 | +import type { ReactNode, Ref } from "react"; | |
| 2 | +import { cx } from "../../utils"; | |
| 3 | + | |
| 4 | +/** `FoxAlert`·`FoxHelperText`와 같은 다섯 계열이다 — 시안이 같은 축을 쓴다. */ | |
| 5 | +export type FoxStatusIndicatorType = | |
| 6 | + | "default" | |
| 7 | + | "information" | |
| 8 | + | "success" | |
| 9 | + | "warning" | |
| 10 | + | "danger"; | |
| 11 | + | |
| 12 | +export interface FoxStatusIndicatorProps { | |
| 13 | + /** 점과 글자의 색이 여기서 갈린다. */ | |
| 14 | + type?: FoxStatusIndicatorType; | |
| 15 | + /** 상태를 나타내는 글자. */ | |
| 16 | + label: ReactNode; | |
| 17 | + /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */ | |
| 18 | + hidden?: boolean; | |
| 19 | + id?: string; | |
| 20 | + /** 배치 조정용. 모양이 달라야 하면 여기 말고 `type`을 쓴다. */ | |
| 21 | + className?: string; | |
| 22 | + ref?: Ref<HTMLSpanElement>; | |
| 23 | +} | |
| 24 | + | |
| 25 | +/** `Record`로 고정해 계열을 추가하면 항목 누락이 타입 에러가 되게 한다. */ | |
| 26 | +const TYPE_CLASS: Record<FoxStatusIndicatorType, string> = { | |
| 27 | + default: "fox-status-indicator--default", | |
| 28 | + information: "fox-status-indicator--information", | |
| 29 | + success: "fox-status-indicator--success", | |
| 30 | + warning: "fox-status-indicator--warning", | |
| 31 | + danger: "fox-status-indicator--danger", | |
| 32 | +}; | |
| 33 | + | |
| 34 | +/** | |
| 35 | + * @fox 상태 표시. 작은 점 하나와 글자로 상태를 알린다. 크기 수식어가 없다 — 시안에 하나뿐이다. | |
| 36 | + * | |
| 37 | + * **점은 장식이다**(`aria-hidden`). 상태의 의미는 글자가 전부 담는다 — 색만으로 뜻을 전하면 | |
| 38 | + * 색을 구분하지 못하는 사용자에게는 아무것도 전달되지 않기 때문이고, 그래서 `label`이 | |
| 39 | + * 선택값이 아니라 필수값이다. | |
| 40 | + * | |
| 41 | + * 값이 바뀌는 것을 소리로 알려야 하는 자리(예: 처리 중 → 완료)라면 감싼 쪽이 live region을 | |
| 42 | + * 만든다 — 이 조각은 상태를 갖지 않으므로 스스로 알리지 않는다. | |
| 43 | + * | |
| 44 | + * 상호작용이 없어 `"use client"`가 아니다 — 서버 컴포넌트로 렌더된다. | |
| 45 | + * | |
| 46 | + * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"` | |
| 47 | + * (또는 개별 파티셜)로 한 번 불러와야 한다. | |
| 48 | + */ | |
| 49 | +export function FoxStatusIndicator({ | |
| 50 | + type = "default", | |
| 51 | + label, | |
| 52 | + hidden = false, | |
| 53 | + id, | |
| 54 | + className, | |
| 55 | + ref, | |
| 56 | +}: FoxStatusIndicatorProps) { | |
| 57 | + if (hidden) { | |
| 58 | + return null; | |
| 59 | + } | |
| 60 | + | |
| 61 | + return ( | |
| 62 | + <span | |
| 63 | + ref={ref} | |
| 64 | + id={id} | |
| 65 | + className={cx("fox-status-indicator", TYPE_CLASS[type], className)} | |
| 66 | + > | |
| 67 | + <span className="fox-status-indicator__dot" aria-hidden="true" /> | |
| 68 | + <span className="fox-status-indicator__label">{label}</span> | |
| 69 | + </span> | |
| 70 | + ); | |
| 71 | +} |
+++ @fox/core/components/fox-status-indicator/index.ts
... | ... | @@ -0,0 +1,5 @@ |
| 1 | +export { | |
| 2 | + FoxStatusIndicator, | |
| 3 | + type FoxStatusIndicatorProps, | |
| 4 | + type FoxStatusIndicatorType, | |
| 5 | +} from "./fox-status-indicator"; |
--- @fox/core/components/index.ts
+++ @fox/core/components/index.ts
... | ... | @@ -39,6 +39,7 @@ |
| 39 | 39 |
export * from "./fox-select-option-item"; |
| 40 | 40 |
export * from "./fox-select-text"; |
| 41 | 41 |
export * from "./fox-spinner"; |
| 42 |
+export * from "./fox-status-indicator"; |
|
| 42 | 43 |
export * from "./fox-tab"; |
| 43 | 44 |
export * from "./fox-tab-item"; |
| 44 | 45 |
export * from "./fox-table"; |
--- @fox/dev-test/component-registry.tsx
+++ @fox/dev-test/component-registry.tsx
... | ... | @@ -53,6 +53,7 @@ |
| 53 | 53 |
import { FoxTab } from "../core/components/fox-tab";
|
| 54 | 54 |
import { FoxTabItem } from "../core/components/fox-tab-item";
|
| 55 | 55 |
import { FoxSpinner } from "../core/components/fox-spinner";
|
| 56 |
+import { FoxStatusIndicator } from "../core/components/fox-status-indicator";
|
|
| 56 | 57 |
import { FoxChip, type FoxChipItem } from "../core/components/fox-chip";
|
| 57 | 58 |
import { FoxChipArea } from "../core/components/fox-chip-area";
|
| 58 | 59 |
import { FoxSelectText } from "../core/components/fox-select-text";
|
... | ... | @@ -3418,4 +3419,32 @@ |
| 3418 | 3419 |
}, |
| 3419 | 3420 |
], |
| 3420 | 3421 |
}, |
| 3422 |
+ {
|
|
| 3423 |
+ id: "fox-status-indicator", |
|
| 3424 |
+ name: "FoxStatusIndicator", |
|
| 3425 |
+ description: |
|
| 3426 |
+ "Figma 시안(status-indicator 404:6328) 글자로 상태를 알립니다. 점은 장식이라 aria-hidden이고 뜻은 글자가 전부 담습니다 — 그래서 label이 선택값이 아니라 필수값입니다. 색만으로 상태를 전하면 색을 구분하지 못하는 사용자에게는 아무것도 전달되지 않습니다.", |
|
| 3427 |
+ variants: [ |
|
| 3428 |
+ {
|
|
| 3429 |
+ label: "계열 5종", |
|
| 3430 |
+ node: ( |
|
| 3431 |
+ <> |
|
| 3432 |
+ <FoxStatusIndicator label="시작전, 중립적" /> |
|
| 3433 |
+ <FoxStatusIndicator type="danger" label="중단, 실패, 위험" /> |
|
| 3434 |
+ <FoxStatusIndicator type="information" label="진행 중" /> |
|
| 3435 |
+ <FoxStatusIndicator type="warning" label="잠재적 위험, 주의" /> |
|
| 3436 |
+ <FoxStatusIndicator type="success" label="진행중, 처리중간단계" /> |
|
| 3437 |
+ </> |
|
| 3438 |
+ ), |
|
| 3439 |
+ }, |
|
| 3440 |
+ {
|
|
| 3441 |
+ label: "inline-flex라 글줄 안에 그대로 놓입니다", |
|
| 3442 |
+ node: ( |
|
| 3443 |
+ <p style={{ margin: 0 }}>
|
|
| 3444 |
+ 현재 상태는 <FoxStatusIndicator type="success" label="정상" /> 입니다. |
|
| 3445 |
+ </p> |
|
| 3446 |
+ ), |
|
| 3447 |
+ }, |
|
| 3448 |
+ ], |
|
| 3449 |
+ }, |
|
| 3421 | 3450 |
]; |
+++ @fox/styles/_fox-status-indicator.scss
... | ... | @@ -0,0 +1,88 @@ |
| 1 | +// FoxStatusIndicator — 시안: 통합관리자페이지 디자인시스템 Figma status-indicator | |
| 2 | +// | |
| 3 | +// 작은 점 하나와 글자로 상태를 알린다. 크기 수식어가 없다 — 시안에 하나뿐이다. | |
| 4 | +// | |
| 5 | +// 마크업 계약 (React 밖 소비자용): | |
| 6 | +// <span class="fox-status-indicator fox-status-indicator--success"> | |
| 7 | +// <span class="fox-status-indicator__dot"></span> | |
| 8 | +// <span class="fox-status-indicator__label">완료</span> | |
| 9 | +// </span> | |
| 10 | +// | |
| 11 | +// 점은 비어 있는 요소다 — 배경색만으로 그린다. 뜻은 글자가 담고 점은 장식이라 | |
| 12 | +// 마크업에서 `aria-hidden`이 붙는다. | |
| 13 | +// | |
| 14 | +// 계열은 **점의 배경과 글자색을 각각 다른 토큰으로** 바꾼다. 점은 `element-*`(칠하는 면), | |
| 15 | +// 글자는 `font-*`(읽는 글자)다 — 같은 계열이라도 명도가 달라 시안이 둘을 나눠 준다. | |
| 16 | +// 기본은 점이 `element-neutral-default`(회색 면), 글자가 `font-neutral-default`다. | |
| 17 | +// | |
| 18 | +// 토큰이 아닌 값: | |
| 19 | +// - line-height / letter-spacing: 시안 텍스트 스타일 label/md의 값이나 Figma가 변수로 | |
| 20 | +// export하지 않는다. 시안의 -0.375px은 15px 기준이라 `-0.025em`으로 적는다 | |
| 21 | +// (다른 컴포넌트와 같은 근거). line-height는 시안이 100%라 `1`이다 — 점과 글자가 한 줄에 | |
| 22 | +// 서는 조각이라 줄 간격을 두지 않는다. | |
| 23 | +// - display / align-items / aspect-ratio / flex-shrink: 구조 규칙. | |
| 24 | + | |
| 25 | +@use "@fox/styles/abstracts" as fox; | |
| 26 | + | |
| 27 | +.fox-status-indicator { | |
| 28 | + display: inline-flex; | |
| 29 | + align-items: center; | |
| 30 | + gap: fox.gap(2); | |
| 31 | + | |
| 32 | + color: fox.color(font-neutral-default); | |
| 33 | + font-family: fox.font-family(body); | |
| 34 | + font-size: fox.font-size(label-md); | |
| 35 | + font-weight: fox.font-weight(regular); | |
| 36 | + line-height: 1; | |
| 37 | + letter-spacing: -0.025em; | |
| 38 | + | |
| 39 | + // 6px 정원. 글자가 길어 줄이 좁아져도 찌그러지지 않게 크기를 못박는다. | |
| 40 | + &__dot { | |
| 41 | + flex-shrink: 0; | |
| 42 | + inline-size: fox.number(4); | |
| 43 | + block-size: fox.number(4); | |
| 44 | + aspect-ratio: 1 / 1; | |
| 45 | + border-radius: fox.radius(max); | |
| 46 | + background: fox.color(element-neutral-default); | |
| 47 | + } | |
| 48 | + | |
| 49 | + // ── 계열 ──────────────────────────────────────────────────────────────────── | |
| 50 | + // 글자색은 블록에서 상속되므로 루트에 주고, 점은 자기 배경을 따로 받는다. | |
| 51 | + // | |
| 52 | + // `default`는 블록 규칙이 곧 그 계열의 모양이라 따로 덮을 것이 없다. | |
| 53 | + &--default { | |
| 54 | + // 시안 그대로 — 블록에 있는 값이 전부다. | |
| 55 | + } | |
| 56 | + | |
| 57 | + &--information { | |
| 58 | + color: fox.color(font-system-information); | |
| 59 | + } | |
| 60 | + | |
| 61 | + &--information &__dot { | |
| 62 | + background: fox.color(element-system-information-strong); | |
| 63 | + } | |
| 64 | + | |
| 65 | + &--success { | |
| 66 | + color: fox.color(font-system-success); | |
| 67 | + } | |
| 68 | + | |
| 69 | + &--success &__dot { | |
| 70 | + background: fox.color(element-system-success-strong); | |
| 71 | + } | |
| 72 | + | |
| 73 | + &--warning { | |
| 74 | + color: fox.color(font-system-warning); | |
| 75 | + } | |
| 76 | + | |
| 77 | + &--warning &__dot { | |
| 78 | + background: fox.color(element-system-warning-strong); | |
| 79 | + } | |
| 80 | + | |
| 81 | + &--danger { | |
| 82 | + color: fox.color(font-system-danger); | |
| 83 | + } | |
| 84 | + | |
| 85 | + &--danger &__dot { | |
| 86 | + background: fox.color(element-system-danger-strong); | |
| 87 | + } | |
| 88 | +} |
--- @fox/styles/components.scss
+++ @fox/styles/components.scss
... | ... | @@ -52,5 +52,6 @@ |
| 52 | 52 |
@use "fox-tab"; |
| 53 | 53 |
@use "fox-tab-item"; |
| 54 | 54 |
@use "fox-spinner"; |
| 55 |
+@use "fox-status-indicator"; |
|
| 55 | 56 |
@use "fox-toast"; |
| 56 | 57 |
@use "fox-toggle-quantity";(파일 끝에 줄바꿈 문자 없음) |
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?