merge: origin/develop — @fox 신규 컴포넌트 6종 반영
충돌 2건은 양쪽이 같은 자리에 서로 다른 것을 **더하기만** 한 경우라 합집합으로 풀었다. - styles/components.scss: hub의 파티셜 목록에 fox-status-indicator·fox-toast를 더했다. - dev-test/component-registry.tsx: 데모 함수(hub의 ModalDemo·DemoThumb + origin/develop의 ToastDemo)와 카탈로그 항목(45 공통 + hub 5 + origin/develop 6 = 56)을 모두 남겼다. git이 두 삽입의 공통 꼬리(`…}`)를 맞물려 놓아 마커 그대로는 함수가 끊긴다 — 그래서 마커를 고치지 않고 양쪽 원본에서 함수·항목 단위로 다시 세웠다. 검증: tsc·eslint 무오류, next build 성공(/dev-test/design이 빌드 시 프리렌더되므로 병합된 레지스트리가 실제로 렌더되는 것까지 확인). Co-Authored-By: Claude Opus 5
@dbc6bcb81150a32d11bc2c2bae4ace17d94f9125
+++ @fox/core/components/fox-alert/fox-alert.tsx
... | ... | @@ -0,0 +1,132 @@ |
| 1 | +import type { ReactNode, Ref } from "react"; | |
| 2 | +import { | |
| 3 | + FoxChatDotsIcon, | |
| 4 | + FoxCheckCircleIcon, | |
| 5 | + FoxInfoIcon, | |
| 6 | + FoxProhibitIcon, | |
| 7 | + FoxWarningIcon, | |
| 8 | +} from "../../icons"; | |
| 9 | +import { cx } from "../../utils"; | |
| 10 | + | |
| 11 | +export type FoxAlertType = | |
| 12 | + | "default" | |
| 13 | + | "information" | |
| 14 | + | "success" | |
| 15 | + | "warning" | |
| 16 | + | "danger"; | |
| 17 | + | |
| 18 | +export interface FoxAlertProps { | |
| 19 | + /** | |
| 20 | + * 알림의 성격. 아이콘·배경·테두리·제목 색이 여기서 갈린다. | |
| 21 | + * | |
| 22 | + * 계열이 바꾸는 것은 배경 · 테두리 · 제목 글자색과 아이콘이다. 본문 글자색은 계열과 | |
| 23 | + * 무관하게 같다. | |
| 24 | + * | |
| 25 | + * 아이콘은 계열이 정하고 굵기는 시안대로 `duotone`이며, 색은 제목 글자와 같은 계열색이다. | |
| 26 | + */ | |
| 27 | + type?: FoxAlertType; | |
| 28 | + /** 굵은 제목 줄. 넘기지 않으면 영역을 렌더링하지 않는다. */ | |
| 29 | + title?: ReactNode; | |
| 30 | + /** 제목 아래 본문. 넘기지 않으면 영역을 렌더링하지 않는다. */ | |
| 31 | + message?: ReactNode; | |
| 32 | + /** | |
| 33 | + * 보조기술이 읽는 방식을 정한다(`FoxToast`와 같은 규칙). | |
| 34 | + * | |
| 35 | + * - `false`(기본): `role="status"` — 하던 말을 끊지 않는다. 화면과 함께 처음부터 놓여 | |
| 36 | + * 있는 알림은 이쪽이다. | |
| 37 | + * - `true`: `role="alert"` — 읽던 것을 끊고 바로 읽는다. 사용자의 동작 결과로 **그 자리에 | |
| 38 | + * 나타난** 알림에만 쓴다. | |
| 39 | + * | |
| 40 | + * `type="danger"`가 자동으로 이걸 켜지 않는다 — 위험한 내용인 것과 지금 끼어들어야 하는 | |
| 41 | + * 것은 다른 문제이고, 페이지와 함께 그려지는 경고까지 낭독을 끊으면 방해만 된다. | |
| 42 | + */ | |
| 43 | + urgent?: boolean; | |
| 44 | + /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */ | |
| 45 | + hidden?: boolean; | |
| 46 | + id?: string; | |
| 47 | + /** 배치 조정용. 시안의 폭(720px)을 바꿔야 할 때도 여기로 준다. */ | |
| 48 | + className?: string; | |
| 49 | + ref?: Ref<HTMLDivElement>; | |
| 50 | +} | |
| 51 | + | |
| 52 | +/** `Record`로 고정해 계열을 추가하면 항목 누락이 타입 에러가 되게 한다. */ | |
| 53 | +const TYPE_CLASS: Record<FoxAlertType, string> = { | |
| 54 | + default: "fox-alert--default", | |
| 55 | + information: "fox-alert--information", | |
| 56 | + success: "fox-alert--success", | |
| 57 | + warning: "fox-alert--warning", | |
| 58 | + danger: "fox-alert--danger", | |
| 59 | +}; | |
| 60 | + | |
| 61 | +/** | |
| 62 | + * 계열이 정하는 아이콘. 호출부가 넘기지 않는다 — 무엇을 그릴지는 계열이 이미 답한다 | |
| 63 | + * (`FoxTag`와 같은 규칙). | |
| 64 | + * | |
| 65 | + * 시안 `ico/*`가 Phosphor 원본이라 이름이 그대로 붙는다(`ico/Prohibit` → | |
| 66 | + * `FoxProhibitIcon`). 손으로 SVG를 그려 넣지 않는다 — 아이콘은 생성물이 단일 출처다. | |
| 67 | + * | |
| 68 | + * 굵기는 시안이 `Weight=Duotone`이라 다섯 개 모두 `duotone`이다. 기본값(`regular`)으로 두면 | |
| 69 | + * 속이 빈 선 그림이 되어 시안과 달라지므로 반드시 적는다. | |
| 70 | + * | |
| 71 | + * duotone은 연한 면(`opacity` 0.2)과 진한 선을 겹쳐 그리는데 **둘 다 `currentColor`다** — | |
| 72 | + * 색은 스타일이 `.fox-alert__icon`에 준 하나를 쓰고, 연한 쪽만 알아서 흐려진다. | |
| 73 | + */ | |
| 74 | +const TYPE_ICON: Record<FoxAlertType, ReactNode> = { | |
| 75 | + default: <FoxChatDotsIcon weight="duotone" />, | |
| 76 | + information: <FoxInfoIcon weight="duotone" />, | |
| 77 | + success: <FoxCheckCircleIcon weight="duotone" />, | |
| 78 | + warning: <FoxWarningIcon weight="duotone" />, | |
| 79 | + danger: <FoxProhibitIcon weight="duotone" />, | |
| 80 | +}; | |
| 81 | + | |
| 82 | +/** | |
| 83 | + * @fox 알럿. 화면 안에 놓이는 띠 모양 알림이다 — 아이콘 상자 · 제목 · 본문이 한 줄로 선다. | |
| 84 | + * | |
| 85 | + * **모달이 아니다.** 화면을 덮지도, 포커스를 가두지도, 확인 버튼을 갖지도 않는다. 내용 흐름 | |
| 86 | + * 안에 그대로 놓이고, 언제 나타나고 사라질지는 호출부가 정한다(`FoxToast`와 같은 방식). | |
| 87 | + * | |
| 88 | + * 상호작용이 없어 `"use client"`가 아니다 — 서버 컴포넌트로 렌더된다. | |
| 89 | + * | |
| 90 | + * ⚠️ 시안 주석: **본문은 두 줄 이내**로 쓴다. 길이를 코드가 막지는 않는다 — 세 줄이 되면 | |
| 91 | + * 아이콘 상자만 가운데 남고 띠가 늘어난다. | |
| 92 | + * | |
| 93 | + * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"` | |
| 94 | + * (또는 개별 파티셜)로 한 번 불러와야 한다. | |
| 95 | + */ | |
| 96 | +export function FoxAlert({ | |
| 97 | + type = "default", | |
| 98 | + title, | |
| 99 | + message, | |
| 100 | + urgent = false, | |
| 101 | + hidden = false, | |
| 102 | + id, | |
| 103 | + className, | |
| 104 | + ref, | |
| 105 | +}: FoxAlertProps) { | |
| 106 | + if (hidden) { | |
| 107 | + return null; | |
| 108 | + } | |
| 109 | + | |
| 110 | + return ( | |
| 111 | + <div | |
| 112 | + ref={ref} | |
| 113 | + id={id} | |
| 114 | + role={urgent ? "alert" : "status"} | |
| 115 | + className={cx("fox-alert", TYPE_CLASS[type], className)} | |
| 116 | + > | |
| 117 | + {/* 아이콘이 정해지기 전에도 상자는 그린다 — 시안이 흰 판을 배치의 일부로 준다. */} | |
| 118 | + <span className="fox-alert__icon" aria-hidden="true"> | |
| 119 | + {TYPE_ICON[type]} | |
| 120 | + </span> | |
| 121 | + | |
| 122 | + <div className="fox-alert__content"> | |
| 123 | + {title === undefined ? null : ( | |
| 124 | + <strong className="fox-alert__title">{title}</strong> | |
| 125 | + )} | |
| 126 | + {message === undefined ? null : ( | |
| 127 | + <span className="fox-alert__message">{message}</span> | |
| 128 | + )} | |
| 129 | + </div> | |
| 130 | + </div> | |
| 131 | + ); | |
| 132 | +} |
+++ @fox/core/components/fox-alert/index.ts
... | ... | @@ -0,0 +1,1 @@ |
| 1 | +export { FoxAlert, type FoxAlertProps, type FoxAlertType } from "./fox-alert"; |
+++ @fox/core/components/fox-helper-text/fox-helper-text.tsx
... | ... | @@ -0,0 +1,105 @@ |
| 1 | +import type { ReactNode, Ref } from "react"; | |
| 2 | +import { | |
| 3 | + FoxChatDotsIcon, | |
| 4 | + FoxCheckCircleIcon, | |
| 5 | + FoxInfoIcon, | |
| 6 | + FoxProhibitIcon, | |
| 7 | + FoxWarningIcon, | |
| 8 | +} from "../../icons"; | |
| 9 | +import { cx } from "../../utils"; | |
| 10 | + | |
| 11 | +/** `FoxAlert`와 같은 다섯 계열이다 — 시안이 두 컴포넌트에 같은 축을 쓴다. */ | |
| 12 | +export type FoxHelperTextType = | |
| 13 | + | "default" | |
| 14 | + | "information" | |
| 15 | + | "success" | |
| 16 | + | "warning" | |
| 17 | + | "danger"; | |
| 18 | + | |
| 19 | +export interface FoxHelperTextProps { | |
| 20 | + /** 글자색과 아이콘이 여기서 갈린다. */ | |
| 21 | + type?: FoxHelperTextType; | |
| 22 | + /** 보여 줄 문구. */ | |
| 23 | + message: ReactNode; | |
| 24 | + /** | |
| 25 | + * 아이콘을 그릴지. 시안은 다섯 계열 모두 아이콘을 달지만, 글자만 필요한 자리가 있어 | |
| 26 | + * 끌 수 있게 둔다 — 폼 컴포넌트의 헬퍼가 지금 아이콘 없이 쓰이는 자리가 많다 | |
| 27 | + * (`FoxInput.messageIcon`이 선택값인 것과 같은 사정). | |
| 28 | + */ | |
| 29 | + showIcon?: boolean; | |
| 30 | + /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */ | |
| 31 | + hidden?: boolean; | |
| 32 | + /** | |
| 33 | + * 입력의 `aria-describedby`가 가리킬 값. 폼 옆에 놓을 때 넘겨서 이어 준다 — | |
| 34 | + * 이어 주지 않으면 화면에만 보이고 소리로는 전달되지 않는다. | |
| 35 | + */ | |
| 36 | + id?: string; | |
| 37 | + /** 배치 조정용. 모양이 달라야 하면 여기 말고 `type`을 쓴다. */ | |
| 38 | + className?: string; | |
| 39 | + ref?: Ref<HTMLParagraphElement>; | |
| 40 | +} | |
| 41 | + | |
| 42 | +/** `Record`로 고정해 계열을 추가하면 항목 누락이 타입 에러가 되게 한다. */ | |
| 43 | +const TYPE_CLASS: Record<FoxHelperTextType, string> = { | |
| 44 | + default: "fox-helper-text--default", | |
| 45 | + information: "fox-helper-text--information", | |
| 46 | + success: "fox-helper-text--success", | |
| 47 | + warning: "fox-helper-text--warning", | |
| 48 | + danger: "fox-helper-text--danger", | |
| 49 | +}; | |
| 50 | + | |
| 51 | +/** | |
| 52 | + * 계열이 정하는 아이콘. 글리프는 `FoxAlert`와 같은 것을 쓰고 **굵기만 다르다** — | |
| 53 | + * 알럿은 `duotone`, 여기는 `regular`다(시안 확인). | |
| 54 | + */ | |
| 55 | +const TYPE_ICON: Record<FoxHelperTextType, ReactNode> = { | |
| 56 | + default: <FoxChatDotsIcon />, | |
| 57 | + information: <FoxInfoIcon />, | |
| 58 | + success: <FoxCheckCircleIcon />, | |
| 59 | + warning: <FoxWarningIcon />, | |
| 60 | + danger: <FoxProhibitIcon />, | |
| 61 | +}; | |
| 62 | + | |
| 63 | +/** | |
| 64 | + * @fox 헬퍼 텍스트. 입력 아래 한 줄로 붙는 안내·오류 문구다. | |
| 65 | + * | |
| 66 | + * 상태를 갖지 않는다 — 어떤 계열로 보일지는 호출부가 정한다. | |
| 67 | + * | |
| 68 | + * TODO(폼 통합, 적용 후 이 문단 삭제): 폼 컴포넌트 여섯 곳(`FoxInput`·`FoxTextArea`· | |
| 69 | + * `FoxEmail`·`FoxPhoneNumber`·`FoxAddress`의 `__message`, `FoxSelect`의 `__hint`)이 | |
| 70 | + * 아직 각자 그린다. 이걸로 모으는 절차와 주의점은 `@fox/styles/_fox-helper-text.scss` | |
| 71 | + * 상단에 적어 두었다 — `FoxSelect`만 간격과 오류 아이콘 색이 달라지므로 확인이 필요하다. | |
| 72 | + * | |
| 73 | + * 상호작용이 없어 `"use client"`가 아니다 — 서버 컴포넌트로 렌더된다. | |
| 74 | + * | |
| 75 | + * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"` | |
| 76 | + * (또는 개별 파티셜)로 한 번 불러와야 한다. | |
| 77 | + */ | |
| 78 | +export function FoxHelperText({ | |
| 79 | + type = "default", | |
| 80 | + message, | |
| 81 | + showIcon = true, | |
| 82 | + hidden = false, | |
| 83 | + id, | |
| 84 | + className, | |
| 85 | + ref, | |
| 86 | +}: FoxHelperTextProps) { | |
| 87 | + if (hidden) { | |
| 88 | + return null; | |
| 89 | + } | |
| 90 | + | |
| 91 | + return ( | |
| 92 | + <p | |
| 93 | + ref={ref} | |
| 94 | + id={id} | |
| 95 | + className={cx("fox-helper-text", TYPE_CLASS[type], className)} | |
| 96 | + > | |
| 97 | + {showIcon ? ( | |
| 98 | + <span className="fox-helper-text__icon" aria-hidden="true"> | |
| 99 | + {TYPE_ICON[type]} | |
| 100 | + </span> | |
| 101 | + ) : null} | |
| 102 | + <span className="fox-helper-text__message">{message}</span> | |
| 103 | + </p> | |
| 104 | + ); | |
| 105 | +} |
+++ @fox/core/components/fox-helper-text/index.ts
... | ... | @@ -0,0 +1,5 @@ |
| 1 | +export { | |
| 2 | + FoxHelperText, | |
| 3 | + type FoxHelperTextProps, | |
| 4 | + type FoxHelperTextType, | |
| 5 | +} from "./fox-helper-text"; |
+++ @fox/core/components/fox-snackbar-list/fox-snackbar-list.tsx
... | ... | @@ -0,0 +1,56 @@ |
| 1 | +import type { ReactNode, Ref } from "react"; | |
| 2 | +import { cx } from "../../utils"; | |
| 3 | + | |
| 4 | +export interface FoxSnackbarListProps { | |
| 5 | + /** 묶음을 읽어 줄 이름. 화면에 보이는 제목이 이미 있으면 `labelledBy`를 쓴다. */ | |
| 6 | + label?: string; | |
| 7 | + /** 묶음 이름 역할을 하는 요소의 id. `label`보다 우선한다 — 화면의 글자와 어긋나지 않는다. */ | |
| 8 | + labelledBy?: string; | |
| 9 | + /** `FoxSnackbar`들. */ | |
| 10 | + children?: ReactNode; | |
| 11 | + /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */ | |
| 12 | + hidden?: boolean; | |
| 13 | + /** 배치 조정용. 띄우는 자리(fixed·bottom 등)와 시안의 폭(640px)을 여기로 덮는다. */ | |
| 14 | + className?: string; | |
| 15 | + ref?: Ref<HTMLDivElement>; | |
| 16 | +} | |
| 17 | + | |
| 18 | +/** | |
| 19 | + * @fox 스낵바 묶음. 스낵바가 여러 개일 때 세로로 쌓는다. 배치만 책임지고 상태를 갖지 않는다 — | |
| 20 | + * 무엇이 몇 개 떠 있는지, 어떤 순서로 쌓을지는 호출부가 소유한다. | |
| 21 | + * | |
| 22 | + * 자식에게 덮어쓰는 값이 없다(`FoxTagList`·`FoxChipArea`와 다른 점이다) — 스낵바에는 계열도 | |
| 23 | + * 크기도 없어서 묶음이 통일할 것이 폭과 간격뿐이다. | |
| 24 | + * | |
| 25 | + * 방향 수식어가 없다 — 스낵바는 세로로 쌓이는 것이 유일한 배치다. | |
| 26 | + * | |
| 27 | + * `role="group"`이다. 각 스낵바가 스스로 `role="status"`로 읽히므로 묶음은 자리만 만든다 — | |
| 28 | + * 묶음까지 live region으로 만들면 하나가 뜰 때마다 목록 전체가 다시 읽힌다. | |
| 29 | + * | |
| 30 | + * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"` | |
| 31 | + * (또는 개별 파티셜)로 한 번 불러와야 한다. | |
| 32 | + */ | |
| 33 | +export function FoxSnackbarList({ | |
| 34 | + label, | |
| 35 | + labelledBy, | |
| 36 | + children, | |
| 37 | + hidden = false, | |
| 38 | + className, | |
| 39 | + ref, | |
| 40 | +}: FoxSnackbarListProps) { | |
| 41 | + if (hidden) { | |
| 42 | + return null; | |
| 43 | + } | |
| 44 | + | |
| 45 | + return ( | |
| 46 | + <div | |
| 47 | + ref={ref} | |
| 48 | + role="group" | |
| 49 | + aria-label={labelledBy ? undefined : label} | |
| 50 | + aria-labelledby={labelledBy} | |
| 51 | + className={cx("fox-snackbar-list", className)} | |
| 52 | + > | |
| 53 | + {children} | |
| 54 | + </div> | |
| 55 | + ); | |
| 56 | +} |
+++ @fox/core/components/fox-snackbar-list/index.ts
... | ... | @@ -0,0 +1,1 @@ |
| 1 | +export { FoxSnackbarList, type FoxSnackbarListProps } from "./fox-snackbar-list"; |
+++ @fox/core/components/fox-snackbar/fox-snackbar.tsx
... | ... | @@ -0,0 +1,131 @@ |
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import type { ReactNode, Ref } from "react"; | |
| 4 | +import { FoxXIcon } from "../../icons"; | |
| 5 | +import { cx } from "../../utils"; | |
| 6 | +import { FoxTextButton } from "../fox-text-button"; | |
| 7 | + | |
| 8 | +export interface FoxSnackbarProps { | |
| 9 | + /** | |
| 10 | + * 글자 앞에 붙는 아이콘. 넘기지 않으면 자리를 만들지 않는다. | |
| 11 | + * | |
| 12 | + * 계열이 없는 조각이라 무엇을 그릴지는 호출부가 정한다(`FoxAlert`처럼 계열이 아이콘을 | |
| 13 | + * 고르지 않는다). `@fox/core/icons`에서 가져다 넘긴다 — 시안은 `fill` 굵기다: | |
| 14 | + * `<FoxCheckCircleIcon weight="fill" />`. | |
| 15 | + * | |
| 16 | + * 색은 스타일이 정한다(`icon-theme-primary`) — 글리프가 `fill: currentColor`라 넘기는 | |
| 17 | + * 쪽에서 색을 줄 필요가 없다. | |
| 18 | + */ | |
| 19 | + icon?: ReactNode; | |
| 20 | + /** 굵은 첫 줄. 넘기지 않으면 영역을 렌더링하지 않는다 — 본문만 있는 스낵바가 된다. */ | |
| 21 | + title?: ReactNode; | |
| 22 | + /** 제목 아래 본문. 넘기지 않으면 영역을 렌더링하지 않는다. */ | |
| 23 | + message?: ReactNode; | |
| 24 | + /** | |
| 25 | + * 오른쪽 글자 버튼의 문구. 없으면 버튼을 그리지 않는다. | |
| 26 | + * | |
| 27 | + * 버튼은 `FoxTextButton`의 `primary`·`md`로 고정이다(시안 지정) — 계열·크기를 열지 않는 | |
| 28 | + * 것은 스낵바마다 버튼 모양이 달라지면 같은 조각으로 읽히지 않기 때문이다. | |
| 29 | + */ | |
| 30 | + actionLabel?: string; | |
| 31 | + /** 글자 버튼을 눌렀을 때. `actionLabel`이 없으면 의미 없다. */ | |
| 32 | + onAction?: () => void; | |
| 33 | + /** | |
| 34 | + * 닫기(X)를 눌렀을 때. **필수다** — 시안이 닫기를 항상 그리므로, 눌러도 아무 일이 없는 | |
| 35 | + * 버튼이 남지 않게 호출부가 반드시 처리를 준다. | |
| 36 | + */ | |
| 37 | + onClose: () => void; | |
| 38 | + /** 닫기 버튼을 읽어 줄 이름. X 아이콘은 장식이라 이 글자가 유일한 이름이다. */ | |
| 39 | + closeLabel?: string; | |
| 40 | + /** | |
| 41 | + * 보조기술이 읽는 방식을 정한다(`FoxToast`·`FoxAlert`와 같은 규칙). | |
| 42 | + * | |
| 43 | + * - `false`(기본): `role="status"` — 하던 말을 끊지 않는다. | |
| 44 | + * - `true`: `role="alert"` — 읽던 것을 끊고 바로 읽는다. 지금 손을 멈춰야 하는 알림에만. | |
| 45 | + */ | |
| 46 | + urgent?: boolean; | |
| 47 | + /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */ | |
| 48 | + hidden?: boolean; | |
| 49 | + id?: string; | |
| 50 | + /** 배치 조정용. 띄우는 자리(fixed·bottom 등)와 시안의 폭(640px)을 여기로 덮는다. */ | |
| 51 | + className?: string; | |
| 52 | + ref?: Ref<HTMLDivElement>; | |
| 53 | +} | |
| 54 | + | |
| 55 | +/** | |
| 56 | + * @fox 스낵바. 흰 판 위에 굵은 한 줄과 글자 버튼이 서고, 오른쪽 끝에 닫기가 붙는다. | |
| 57 | + * 크기 수식어가 없다 — 시안에 하나뿐이다. | |
| 58 | + * | |
| 59 | + * `FoxToast`와 형제지만 **누를 것이 있다.** 토스트는 읽고 지나가는 조각이라 스스로 사라지고, | |
| 60 | + * 스낵바는 사용자가 되돌리거나(실행 취소) 이어서 할 일이 있어 손이 닿을 때까지 남는다. | |
| 61 | + * | |
| 62 | + * **상태를 갖지 않는다.** 언제 뜨고 언제 사라질지, 화면 어디에 놓일지는 전부 호출부가 정한다 | |
| 63 | + * — `onClose`는 "닫아 달라"는 알림이지 이 조각이 스스로 사라지는 것이 아니다. | |
| 64 | + * | |
| 65 | + * ⚠️ 보조기술은 **이미 화면에 있던 live region의 내용이 바뀔 때** 읽는다. 이 요소를 통째로 | |
| 66 | + * 새로 붙이면 조합에 따라 읽히지 않을 수 있다(`FoxToast`와 같은 사정). | |
| 67 | + * | |
| 68 | + * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"` | |
| 69 | + * (또는 개별 파티셜)로 한 번 불러와야 한다. | |
| 70 | + */ | |
| 71 | +export function FoxSnackbar({ | |
| 72 | + icon, | |
| 73 | + title, | |
| 74 | + message, | |
| 75 | + actionLabel, | |
| 76 | + onAction, | |
| 77 | + onClose, | |
| 78 | + closeLabel = "닫기", | |
| 79 | + urgent = false, | |
| 80 | + hidden = false, | |
| 81 | + id, | |
| 82 | + className, | |
| 83 | + ref, | |
| 84 | +}: FoxSnackbarProps) { | |
| 85 | + if (hidden) { | |
| 86 | + return null; | |
| 87 | + } | |
| 88 | + | |
| 89 | + return ( | |
| 90 | + <div | |
| 91 | + ref={ref} | |
| 92 | + id={id} | |
| 93 | + role={urgent ? "alert" : "status"} | |
| 94 | + className={cx("fox-snackbar", className)} | |
| 95 | + > | |
| 96 | + {icon ? ( | |
| 97 | + <span className="fox-snackbar__icon" aria-hidden="true"> | |
| 98 | + {icon} | |
| 99 | + </span> | |
| 100 | + ) : null} | |
| 101 | + | |
| 102 | + <div className="fox-snackbar__content"> | |
| 103 | + {title === undefined ? null : ( | |
| 104 | + <strong className="fox-snackbar__title">{title}</strong> | |
| 105 | + )} | |
| 106 | + {message === undefined ? null : ( | |
| 107 | + <span className="fox-snackbar__message">{message}</span> | |
| 108 | + )} | |
| 109 | + </div> | |
| 110 | + | |
| 111 | + {actionLabel ? ( | |
| 112 | + <FoxTextButton | |
| 113 | + type="primary" | |
| 114 | + size="md" | |
| 115 | + label={actionLabel} | |
| 116 | + onAction={onAction} | |
| 117 | + className="fox-snackbar__action" | |
| 118 | + /> | |
| 119 | + ) : null} | |
| 120 | + | |
| 121 | + <button | |
| 122 | + type="button" | |
| 123 | + className="fox-snackbar__close" | |
| 124 | + aria-label={closeLabel} | |
| 125 | + onClick={onClose} | |
| 126 | + > | |
| 127 | + <FoxXIcon /> | |
| 128 | + </button> | |
| 129 | + </div> | |
| 130 | + ); | |
| 131 | +} |
+++ @fox/core/components/fox-snackbar/index.ts
... | ... | @@ -0,0 +1,1 @@ |
| 1 | +export { FoxSnackbar, type FoxSnackbarProps } from "./fox-snackbar"; |
+++ @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/fox-toast/fox-toast.tsx
... | ... | @@ -0,0 +1,67 @@ |
| 1 | +import type { ReactNode, Ref } from "react"; | |
| 2 | +import { cx } from "../../utils"; | |
| 3 | + | |
| 4 | +export interface FoxToastProps { | |
| 5 | + /** | |
| 6 | + * 알림 문구. | |
| 7 | + * | |
| 8 | + * 이름이 `label`이 아니다 — 다른 컴포넌트의 `label`은 컨트롤에 붙는 이름이지만, 이쪽은 | |
| 9 | + * 조각 안에 담기는 **내용**이라 성격이 다르다. | |
| 10 | + */ | |
| 11 | + message: ReactNode; | |
| 12 | + /** | |
| 13 | + * 보조기술이 읽는 방식을 정한다. | |
| 14 | + * | |
| 15 | + * - `false`(기본): `role="status"` — 하던 말을 끊지 않고 차례가 오면 읽는다. 저장 완료처럼 | |
| 16 | + * 놓쳐도 되는 알림. | |
| 17 | + * - `true`: `role="alert"` — 읽던 것을 끊고 바로 읽는다. 지금 손을 멈춰야 하는 알림에만 | |
| 18 | + * 쓴다. 남발하면 화면 낭독이 계속 끊긴다. | |
| 19 | + */ | |
| 20 | + urgent?: boolean; | |
| 21 | + /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */ | |
| 22 | + hidden?: boolean; | |
| 23 | + id?: string; | |
| 24 | + /** 배치 조정용. 띄우는 위치(fixed·bottom 등)를 여기로 준다. */ | |
| 25 | + className?: string; | |
| 26 | + ref?: Ref<HTMLDivElement>; | |
| 27 | +} | |
| 28 | + | |
| 29 | +/** | |
| 30 | + * @fox 토스트. 어두운 판 위에 한 줄짜리 알림을 얹는다. 크기 수식어가 없다 — 시안에 하나뿐이다. | |
| 31 | + * | |
| 32 | + * **상태를 갖지 않는다.** 언제 뜨고 언제 사라질지, 화면 어디에 놓일지는 전부 호출부가 정한다 | |
| 33 | + * (사용자 결정) — 이 조각은 조건부로 렌더하고 `className`으로 자리를 잡아 주면 된다. 타이머와 | |
| 34 | + * 위치를 안에 두면 SCSS만 쓰는 소비자가 그 절반을 못 쓰고, 뒤이어 만들 스낵바와 규칙이 갈린다. | |
| 35 | + * | |
| 36 | + * 상호작용이 없어 `"use client"`가 아니다 — 서버 컴포넌트로 렌더된다. | |
| 37 | + * | |
| 38 | + * ⚠️ 보조기술은 **이미 화면에 있던 live region의 내용이 바뀔 때** 읽는다. 이 요소를 통째로 새로 | |
| 39 | + * 붙이면 브라우저·리더 조합에 따라 읽히지 않을 수 있다. 확실히 읽혀야 하는 자리라면 호출부가 | |
| 40 | + * 빈 `<div role="status">`를 미리 두고 그 안에서 토스트를 갈아 끼운다. | |
| 41 | + * | |
| 42 | + * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"` | |
| 43 | + * (또는 개별 파티셜)로 한 번 불러와야 한다. | |
| 44 | + */ | |
| 45 | +export function FoxToast({ | |
| 46 | + message, | |
| 47 | + urgent = false, | |
| 48 | + hidden = false, | |
| 49 | + id, | |
| 50 | + className, | |
| 51 | + ref, | |
| 52 | +}: FoxToastProps) { | |
| 53 | + if (hidden) { | |
| 54 | + return null; | |
| 55 | + } | |
| 56 | + | |
| 57 | + return ( | |
| 58 | + <div | |
| 59 | + ref={ref} | |
| 60 | + id={id} | |
| 61 | + role={urgent ? "alert" : "status"} | |
| 62 | + className={cx("fox-toast", className)} | |
| 63 | + > | |
| 64 | + {message} | |
| 65 | + </div> | |
| 66 | + ); | |
| 67 | +} |
+++ @fox/core/components/fox-toast/index.ts
... | ... | @@ -0,0 +1,1 @@ |
| 1 | +export { FoxToast, type FoxToastProps } from "./fox-toast"; |
--- @fox/core/components/index.ts
+++ @fox/core/components/index.ts
... | ... | @@ -1,5 +1,6 @@ |
| 1 | 1 |
export * from "./fox-accordion"; |
| 2 | 2 |
export * from "./fox-address"; |
| 3 |
+export * from "./fox-alert"; |
|
| 3 | 4 |
export * from "./fox-badge"; |
| 4 | 5 |
export * from "./fox-badge-dot"; |
| 5 | 6 |
export * from "./fox-badge-group"; |
... | ... | @@ -25,6 +26,7 @@ |
| 25 | 26 |
export * from "./fox-email"; |
| 26 | 27 |
export * from "./fox-file-upload"; |
| 27 | 28 |
export * from "./fox-form-label"; |
| 29 |
+export * from "./fox-helper-text"; |
|
| 28 | 30 |
export * from "./fox-icon-button"; |
| 29 | 31 |
export * from "./fox-input"; |
| 30 | 32 |
export * from "./fox-link-button"; |
... | ... | @@ -41,7 +43,10 @@ |
| 41 | 43 |
export * from "./fox-select-option"; |
| 42 | 44 |
export * from "./fox-select-option-item"; |
| 43 | 45 |
export * from "./fox-select-text"; |
| 46 |
+export * from "./fox-snackbar"; |
|
| 47 |
+export * from "./fox-snackbar-list"; |
|
| 44 | 48 |
export * from "./fox-spinner"; |
| 49 |
+export * from "./fox-status-indicator"; |
|
| 45 | 50 |
export * from "./fox-tab"; |
| 46 | 51 |
export * from "./fox-tab-item"; |
| 47 | 52 |
export * from "./fox-table"; |
... | ... | @@ -51,6 +56,7 @@ |
| 51 | 56 |
export * from "./fox-tag-list"; |
| 52 | 57 |
export * from "./fox-text-area"; |
| 53 | 58 |
export * from "./fox-text-button"; |
| 59 |
+export * from "./fox-toast"; |
|
| 54 | 60 |
export * from "./fox-toggle-quantity"; |
| 55 | 61 |
export * from "./fox-toggle-switch"; |
| 56 | 62 |
export * from "./fox-list-search"; |
--- @fox/dev-test/component-registry.tsx
+++ @fox/dev-test/component-registry.tsx
... | ... | @@ -1,8 +1,9 @@ |
| 1 | 1 |
"use client"; |
| 2 | 2 |
|
| 3 | 3 |
import { FoxInput, FoxInputState, FoxSelect, FoxSelectItem, FoxSelectOption, FoxSelectOptionItem } from "@/@fox/core/components";
|
| 4 |
-import { useState, type CSSProperties, type ReactNode } from "react";
|
|
| 4 |
+import { useEffect, useState, type CSSProperties, type ReactNode } from "react";
|
|
| 5 | 5 |
import { FoxAddress, type FoxAddressSearchResult, type FoxAddressValue } from "../core/components/fox-address";
|
| 6 |
+import { FoxAlert } from "../core/components/fox-alert";
|
|
| 6 | 7 |
import { FoxBreadcrumb } from "../core/components/fox-breadcrumb";
|
| 7 | 8 |
import { FoxButton } from "../core/components/fox-button";
|
| 8 | 9 |
import { FoxButtonGroup } from "../core/components/fox-button-group";
|
... | ... | @@ -18,6 +19,7 @@ |
| 18 | 19 |
import { FoxEmail } from "../core/components/fox-email";
|
| 19 | 20 |
import { FoxFileUpload, type FoxFileItem } from "../core/components/fox-file-upload";
|
| 20 | 21 |
import { FoxFormLabel } from "../core/components/fox-form-label";
|
| 22 |
+import { FoxHelperText } from "../core/components/fox-helper-text";
|
|
| 21 | 23 |
import { FoxIconButton } from "../core/components/fox-icon-button";
|
| 22 | 24 |
import { FoxLinkButton } from "../core/components/fox-link-button";
|
| 23 | 25 |
import { FoxPagination } from "../core/components/fox-pagination";
|
... | ... | @@ -27,6 +29,7 @@ |
| 27 | 29 |
import { FoxRadioIcon } from "../core/components/fox-radio-icon";
|
| 28 | 30 |
import { FoxTag } from "../core/components/fox-tag";
|
| 29 | 31 |
import { FoxTagList } from "../core/components/fox-tag-list";
|
| 32 |
+import { FoxToast } from "../core/components/fox-toast";
|
|
| 30 | 33 |
import { FoxTextArea, type FoxTextAreaState } from "../core/components/fox-text-area";
|
| 31 | 34 |
import { FoxTextButton } from "../core/components/fox-text-button";
|
| 32 | 35 |
import { FoxToggleSwitch } from "../core/components/fox-toggle-switch";
|
... | ... | @@ -50,6 +53,10 @@ |
| 50 | 53 |
import { FoxTab } from "../core/components/fox-tab";
|
| 51 | 54 |
import { FoxTabItem } from "../core/components/fox-tab-item";
|
| 52 | 55 |
import { FoxSpinner } from "../core/components/fox-spinner";
|
| 56 |
+import { FoxCheckCircleIcon } from "../core/icons";
|
|
| 57 |
+import { FoxSnackbar } from "../core/components/fox-snackbar";
|
|
| 58 |
+import { FoxSnackbarList } from "../core/components/fox-snackbar-list";
|
|
| 59 |
+import { FoxStatusIndicator } from "../core/components/fox-status-indicator";
|
|
| 53 | 60 |
import { FoxChip, type FoxChipItem } from "../core/components/fox-chip";
|
| 54 | 61 |
import { FoxChipArea } from "../core/components/fox-chip-area";
|
| 55 | 62 |
import { FoxSelectText } from "../core/components/fox-select-text";
|
... | ... | @@ -601,7 +608,6 @@ |
| 601 | 608 |
); |
| 602 | 609 |
} |
| 603 | 610 |
|
| 604 |
-/** 모달은 열어 봐야 보이므로 여는 버튼과 함께 낸다. */ |
|
| 605 | 611 |
function ModalDemo({
|
| 606 | 612 |
size, |
| 607 | 613 |
title, |
... | ... | @@ -654,11 +660,6 @@ |
| 654 | 660 |
); |
| 655 | 661 |
} |
| 656 | 662 |
|
| 657 |
-/** |
|
| 658 |
- * 그림 자리 확인용 도형. 실제 사진 대신 쓴다 — `@fox`는 앱의 이미지 경로를 참조하지 않고 |
|
| 659 |
- * (다른 프로젝트로 통째로 복사되어야 한다), 카탈로그가 외부 파일에 기대지도 않게 한다. |
|
| 660 |
- * 토큰 색을 그대로 쓰므로 라이트·다크에서 함께 바뀐다. |
|
| 661 |
- */ |
|
| 662 | 663 |
function DemoThumb() {
|
| 663 | 664 |
return ( |
| 664 | 665 |
<svg viewBox="0 0 320 180" preserveAspectRatio="xMidYMid slice" aria-hidden="true"> |
... | ... | @@ -670,6 +671,41 @@ |
| 670 | 671 |
opacity="0.45" |
| 671 | 672 |
/> |
| 672 | 673 |
</svg> |
| 674 |
+ ); |
|
| 675 |
+} |
|
| 676 |
+ |
|
| 677 |
+function ToastDemo() {
|
|
| 678 |
+ const [open, setOpen] = useState(false); |
|
| 679 |
+ |
|
| 680 |
+ useEffect(() => {
|
|
| 681 |
+ if (!open) return; |
|
| 682 |
+ const timer = window.setTimeout(() => setOpen(false), 3000); |
|
| 683 |
+ return () => window.clearTimeout(timer); |
|
| 684 |
+ }, [open]); |
|
| 685 |
+ |
|
| 686 |
+ return ( |
|
| 687 |
+ <> |
|
| 688 |
+ <FoxButton |
|
| 689 |
+ size="md" |
|
| 690 |
+ type="primary" |
|
| 691 |
+ label="토스트 띄우기" |
|
| 692 |
+ onAction={() => setOpen(true)}
|
|
| 693 |
+ disabled={open}
|
|
| 694 |
+ /> |
|
| 695 |
+ {open ? (
|
|
| 696 |
+ <div |
|
| 697 |
+ style={{
|
|
| 698 |
+ position: "fixed", |
|
| 699 |
+ insetInlineStart: "50%", |
|
| 700 |
+ insetBlockEnd: "4rem", |
|
| 701 |
+ transform: "translateX(-50%)", |
|
| 702 |
+ zIndex: 100, |
|
| 703 |
+ }} |
|
| 704 |
+ > |
|
| 705 |
+ <FoxToast message="저장되었습니다." /> |
|
| 706 |
+ </div> |
|
| 707 |
+ ) : null} |
|
| 708 |
+ </> |
|
| 673 | 709 |
); |
| 674 | 710 |
} |
| 675 | 711 |
|
... | ... | @@ -3555,4 +3591,246 @@ |
| 3555 | 3591 |
}, |
| 3556 | 3592 |
], |
| 3557 | 3593 |
}, |
| 3594 |
+ {
|
|
| 3595 |
+ id: "fox-toast", |
|
| 3596 |
+ name: "FoxToast", |
|
| 3597 |
+ description: |
|
| 3598 |
+ "Figma 시안(toast 441:5471) 토스트메세지 — 상태를 갖지 않습니다: 언제 뜨고 언제 사라질지, 화면 어디에 놓일지는 전부 호출부가 정합니다. 타이머와 위치를 안에 두면 SCSS만 쓰는 소비자가 그 절반을 못 쓰고, 뒤이어 만들 스낵바와 규칙이 갈립니다. 배경·글자색이 *-inverse 토큰이라 다크 모드에서는 둘이 함께 뒤집혀 밝은 판 + 어두운 글자가 됩니다.", |
|
| 3599 |
+ variants: [ |
|
| 3600 |
+ {
|
|
| 3601 |
+ label: "기본", |
|
| 3602 |
+ node: <FoxToast message="저장되었습니다." />, |
|
| 3603 |
+ }, |
|
| 3604 |
+ {
|
|
| 3605 |
+ label: "문구가 길면 폭이 내용만큼 늘어납니다 (시안에 상한이 없어 넣지 않았습니다)", |
|
| 3606 |
+ node: <FoxToast message="입력하신 내용을 저장했습니다. 목록에서 확인할 수 있습니다." />, |
|
| 3607 |
+ }, |
|
| 3608 |
+ {
|
|
| 3609 |
+ label: "urgent — 모양은 같고 role만 status에서 alert로 바뀝니다 (읽던 것을 끊고 바로 읽음)", |
|
| 3610 |
+ node: <FoxToast message="네트워크 연결이 끊겼습니다." urgent />, |
|
| 3611 |
+ }, |
|
| 3612 |
+ {
|
|
| 3613 |
+ label: "띄우고 지우는 것은 호출부 몫입니다 — 눌러 보세요 (3초 뒤 사라짐, 화면 하단 고정)", |
|
| 3614 |
+ node: <ToastDemo />, |
|
| 3615 |
+ }, |
|
| 3616 |
+ ], |
|
| 3617 |
+ }, |
|
| 3618 |
+ {
|
|
| 3619 |
+ id: "fox-alert", |
|
| 3620 |
+ name: "FoxAlert", |
|
| 3621 |
+ description: |
|
| 3622 |
+ "Figma 시안(alert 258:433) 폭 720px은 시안 값 그대로지만 좁은 자리에서 부모를 뚫지 않게 max-inline-size: 100%를 함께 걸었습니다. 계열은 배경·테두리·제목 글자색을 바꿉니다 — 테두리만 -inverse 토큰이라 연한 배경(5) 위에 한 단계 진한 선(10)이 얹힙니다. 본문 글자색과 아이콘 상자의 흰 배경은 계열과 무관합니다.", |
|
| 3623 |
+ variants: [ |
|
| 3624 |
+ {
|
|
| 3625 |
+ label: "default", |
|
| 3626 |
+ node: ( |
|
| 3627 |
+ <FoxAlert |
|
| 3628 |
+ title="알림 제목이 들어갑니다" |
|
| 3629 |
+ message="콘텐츠 내 알럿은 최대 2줄 이내로 작성합니다 콘텐츠 내 알럿은 최대 2줄 이내로 작성합니다 콘텐츠 내 알럿은 최대 2줄 이내로 작성합니다" |
|
| 3630 |
+ /> |
|
| 3631 |
+ ), |
|
| 3632 |
+ }, |
|
| 3633 |
+ {
|
|
| 3634 |
+ label: "제목만 / 본문만 — 넘기지 않은 쪽은 렌더하지 않습니다", |
|
| 3635 |
+ node: ( |
|
| 3636 |
+ <> |
|
| 3637 |
+ <FoxAlert title="제목만 있는 알림" /> |
|
| 3638 |
+ <FoxAlert message="본문만 있는 알림입니다." /> |
|
| 3639 |
+ </> |
|
| 3640 |
+ ), |
|
| 3641 |
+ }, |
|
| 3642 |
+ {
|
|
| 3643 |
+ label: "type 5종", |
|
| 3644 |
+ node: ( |
|
| 3645 |
+ <> |
|
| 3646 |
+ <FoxAlert type="default" title="default" message="기본" /> |
|
| 3647 |
+ <FoxAlert type="information" title="information" message="안내" /> |
|
| 3648 |
+ <FoxAlert type="success" title="success" message="완료" /> |
|
| 3649 |
+ <FoxAlert type="warning" title="warning" message="주의" /> |
|
| 3650 |
+ <FoxAlert type="danger" title="danger" message="위험" /> |
|
| 3651 |
+ </> |
|
| 3652 |
+ ), |
|
| 3653 |
+ }, |
|
| 3654 |
+ {
|
|
| 3655 |
+ label: "좁은 자리(360px)에 넣으면 폭이 부모에 맞춰 줄어듭니다", |
|
| 3656 |
+ node: ( |
|
| 3657 |
+ <div style={{ inlineSize: "36rem", outline: "1px dashed currentColor" }}>
|
|
| 3658 |
+ <FoxAlert |
|
| 3659 |
+ title="좁은 단 안의 알림" |
|
| 3660 |
+ message="720px이 아니라 부모 폭까지만 늘어납니다." |
|
| 3661 |
+ /> |
|
| 3662 |
+ </div> |
|
| 3663 |
+ ), |
|
| 3664 |
+ }, |
|
| 3665 |
+ ], |
|
| 3666 |
+ }, |
|
| 3667 |
+ {
|
|
| 3668 |
+ id: "fox-helper-text", |
|
| 3669 |
+ name: "FoxHelperText", |
|
| 3670 |
+ description: |
|
| 3671 |
+ "Figma 시안(helper-txt) 입력 아래 붙는 안내·오류 한 줄입니다. 계열은 FoxAlert와 같은 다섯 개이고 글리프도 같은 것을 쓰되 굵기만 regular입니다(알럿은 duotone). 계열이 붙으면 글자는 font-system-*, 아이콘은 한 단계 진한 icon-system-*-strong입니다 — default만 글자·아이콘이 같은 회색입니다. 모양 값을 새로 적지 않았습니다 — 시안 값이 _form-field.scss의 message 믹스인과 한 자도 다르지 않아(gap 4px · 아이콘 padding-top 2px · body/sm · 150% · -0.025em) 그대로 부릅니다. 차이는 display 하나뿐이라(시안 inline-flex, 폼 안 flex) 믹스인에 인자로 넘깁니다. ⚠️ 폼 컴포넌트 안의 헬퍼는 아직 이 클래스를 쓰지 않습니다 — FoxInput·FoxTextArea·FoxEmail·FoxPhoneNumber·FoxAddress는 각자 __message를, FoxSelect는 __hint를 그리고, 그쪽 색은 상자의 포커스·오류를 :has()로 따라갑니다(auto-states 믹스인). 모으는 것은 별도 작업입니다.", |
|
| 3672 |
+ variants: [ |
|
| 3673 |
+ {
|
|
| 3674 |
+ label: "계열 5종 — 아이콘이 글자보다 한 단계 진합니다 (icon-system-*-strong)", |
|
| 3675 |
+ node: ( |
|
| 3676 |
+ <> |
|
| 3677 |
+ <FoxHelperText message="기본 안내 문구입니다." /> |
|
| 3678 |
+ <FoxHelperText type="information" message="입력 형식을 확인해 주세요." /> |
|
| 3679 |
+ <FoxHelperText type="success" message="사용할 수 있는 아이디입니다." /> |
|
| 3680 |
+ <FoxHelperText type="warning" message="곧 만료되는 항목입니다." /> |
|
| 3681 |
+ <FoxHelperText type="danger" message="필수 항목입니다." /> |
|
| 3682 |
+ </> |
|
| 3683 |
+ ), |
|
| 3684 |
+ }, |
|
| 3685 |
+ {
|
|
| 3686 |
+ label: "showIcon={false} — 글자만 (폼 헬퍼가 아이콘 없이 쓰이는 자리를 위해)",
|
|
| 3687 |
+ node: ( |
|
| 3688 |
+ <> |
|
| 3689 |
+ <FoxHelperText showIcon={false} message="2자 이상 입력해 주세요." />
|
|
| 3690 |
+ <FoxHelperText type="danger" showIcon={false} message="필수 항목입니다." />
|
|
| 3691 |
+ </> |
|
| 3692 |
+ ), |
|
| 3693 |
+ }, |
|
| 3694 |
+ {
|
|
| 3695 |
+ label: "inline-flex라 폭이 글자만큼입니다 (점선은 자리 확인용)", |
|
| 3696 |
+ node: ( |
|
| 3697 |
+ <div style={{ outline: "1px dashed currentColor" }}>
|
|
| 3698 |
+ <FoxHelperText type="information" message="내용만큼만 차지합니다." /> |
|
| 3699 |
+ </div> |
|
| 3700 |
+ ), |
|
| 3701 |
+ }, |
|
| 3702 |
+ {
|
|
| 3703 |
+ label: "여러 줄 — 아이콘이 첫 줄에 맞춰 고정됩니다", |
|
| 3704 |
+ node: ( |
|
| 3705 |
+ <div style={{ inlineSize: "24rem" }}>
|
|
| 3706 |
+ <FoxHelperText |
|
| 3707 |
+ type="warning" |
|
| 3708 |
+ message="문구가 길어져 두 줄이 되어도 아이콘은 위로 붙어 첫 글줄과 나란히 남습니다." |
|
| 3709 |
+ /> |
|
| 3710 |
+ </div> |
|
| 3711 |
+ ), |
|
| 3712 |
+ }, |
|
| 3713 |
+ ], |
|
| 3714 |
+ }, |
|
| 3715 |
+ {
|
|
| 3716 |
+ id: "fox-status-indicator", |
|
| 3717 |
+ name: "FoxStatusIndicator", |
|
| 3718 |
+ description: |
|
| 3719 |
+ "Figma 시안(status-indicator 404:6328) 글자로 상태를 알립니다. 점은 장식이라 aria-hidden이고 뜻은 글자가 전부 담습니다 — 그래서 label이 선택값이 아니라 필수값입니다. 색만으로 상태를 전하면 색을 구분하지 못하는 사용자에게는 아무것도 전달되지 않습니다.", |
|
| 3720 |
+ variants: [ |
|
| 3721 |
+ {
|
|
| 3722 |
+ label: "계열 5종", |
|
| 3723 |
+ node: ( |
|
| 3724 |
+ <> |
|
| 3725 |
+ <FoxStatusIndicator label="시작전, 중립적" /> |
|
| 3726 |
+ <FoxStatusIndicator type="danger" label="중단, 실패, 위험" /> |
|
| 3727 |
+ <FoxStatusIndicator type="information" label="진행 중" /> |
|
| 3728 |
+ <FoxStatusIndicator type="warning" label="잠재적 위험, 주의" /> |
|
| 3729 |
+ <FoxStatusIndicator type="success" label="진행중, 처리중간단계" /> |
|
| 3730 |
+ </> |
|
| 3731 |
+ ), |
|
| 3732 |
+ }, |
|
| 3733 |
+ {
|
|
| 3734 |
+ label: "inline-flex라 글줄 안에 그대로 놓입니다", |
|
| 3735 |
+ node: ( |
|
| 3736 |
+ <p style={{ margin: 0 }}>
|
|
| 3737 |
+ 현재 상태는 <FoxStatusIndicator type="success" label="정상" /> 입니다. |
|
| 3738 |
+ </p> |
|
| 3739 |
+ ), |
|
| 3740 |
+ }, |
|
| 3741 |
+ ], |
|
| 3742 |
+ }, |
|
| 3743 |
+ {
|
|
| 3744 |
+ id: "fox-snackbar", |
|
| 3745 |
+ name: "FoxSnackbar", |
|
| 3746 |
+ description: |
|
| 3747 |
+ "Figma 시안(snackbar 441:5880) 흰 판 위에 굵은 한 줄과 글자 버튼이 서고 오른쪽 끝에 닫기가 붙습니다. FoxToast와 형제지만 누를 것이 있어서, 읽고 지나가는 토스트와 달리 손이 닿을 때까지 남습니다. 상태를 갖지 않습니다 — onClose는 '닫아 달라'는 알림이지 스스로 사라지는 것이 아니고, 위치도 호출부가 정합니다. 글자 버튼은 FoxTextButton의 primary·md, 닫기는 오른쪽에 붙습니다 — 판의 오른쪽 여백 64px은 글자가 그 아래로 들어가지 않게 비워 두는 자리입니다.", |
|
| 3748 |
+ variants: [ |
|
| 3749 |
+ {
|
|
| 3750 |
+ label: "기본 — 제목 + 본문 + 액션 + 닫기", |
|
| 3751 |
+ node: ( |
|
| 3752 |
+ <FoxSnackbar |
|
| 3753 |
+ title="메세지 타이틀" |
|
| 3754 |
+ message="스낵바는 화면 하단에 제공되는 안내 메시지입니다." |
|
| 3755 |
+ actionLabel="텍스트 버튼" |
|
| 3756 |
+ onAction={() => {}}
|
|
| 3757 |
+ onClose={() => {}}
|
|
| 3758 |
+ /> |
|
| 3759 |
+ ), |
|
| 3760 |
+ }, |
|
| 3761 |
+ {
|
|
| 3762 |
+ label: "아이콘 — 호출부가 @fox/core/icons에서 골라 넘깁니다 (ico/CheckCircle, weight=fill / 색은 icon-theme-primary)", |
|
| 3763 |
+ node: ( |
|
| 3764 |
+ <FoxSnackbar |
|
| 3765 |
+ icon={<FoxCheckCircleIcon weight="fill" />}
|
|
| 3766 |
+ title="변경 사항을 저장했습니다" |
|
| 3767 |
+ message="3분 안에는 되돌릴 수 있습니다." |
|
| 3768 |
+ actionLabel="실행 취소" |
|
| 3769 |
+ onAction={() => {}}
|
|
| 3770 |
+ onClose={() => {}}
|
|
| 3771 |
+ /> |
|
| 3772 |
+ ), |
|
| 3773 |
+ }, |
|
| 3774 |
+ {
|
|
| 3775 |
+ label: "제목 없이 / 액션 없이 — 넘기지 않은 쪽은 그리지 않습니다 (닫기는 항상 있습니다)", |
|
| 3776 |
+ node: ( |
|
| 3777 |
+ <> |
|
| 3778 |
+ <FoxSnackbar title="제목만 있는 스낵바" actionLabel="보기" onAction={() => {}} onClose={() => {}} />
|
|
| 3779 |
+ <FoxSnackbar message="제목 없이 본문만 있는 스낵바입니다." actionLabel="보기" onAction={() => {}} onClose={() => {}} />
|
|
| 3780 |
+ <FoxSnackbar title="액션이 없는 스낵바" message="닫기만 있습니다." onClose={() => {}} />
|
|
| 3781 |
+ </> |
|
| 3782 |
+ ), |
|
| 3783 |
+ }, |
|
| 3784 |
+ {
|
|
| 3785 |
+ label: "제목이 길면 줄바꿈되고 버튼은 오른쪽에 남습니다", |
|
| 3786 |
+ node: ( |
|
| 3787 |
+ <FoxSnackbar |
|
| 3788 |
+ title="제목이 길어져 두 줄이 되어도" |
|
| 3789 |
+ message="글자 버튼과 닫기는 자리를 지킵니다. 판은 640px에서 시작하고 오른쪽 64px은 닫기 자리로 비워 둡니다." |
|
| 3790 |
+ actionLabel="자세히" |
|
| 3791 |
+ onAction={() => {}}
|
|
| 3792 |
+ onClose={() => {}}
|
|
| 3793 |
+ /> |
|
| 3794 |
+ ), |
|
| 3795 |
+ }, |
|
| 3796 |
+ ], |
|
| 3797 |
+ }, |
|
| 3798 |
+ {
|
|
| 3799 |
+ id: "fox-snackbar-list", |
|
| 3800 |
+ name: "FoxSnackbarList", |
|
| 3801 |
+ description: |
|
| 3802 |
+ "Figma 시안(snackbar-list 467:5958) 스낵바가 여러 개일 때 세로로 쌓는 묶음입니다. — 무엇이 몇 개 떠 있는지, 어떤 순서로 쌓을지는 호출부가 소유합니다. 자식에게 덮어쓰는 값이 없다는 점이 FoxTagList·FoxChipArea와 다릅니다.", |
|
| 3803 |
+ variants: [ |
|
| 3804 |
+ {
|
|
| 3805 |
+ label: "세 개가 쌓인 모습 (간격 8px)", |
|
| 3806 |
+ node: ( |
|
| 3807 |
+ <FoxSnackbarList label="알림"> |
|
| 3808 |
+ <FoxSnackbar |
|
| 3809 |
+ icon={<FoxCheckCircleIcon weight="fill" />}
|
|
| 3810 |
+ title="변경 사항을 저장했습니다" |
|
| 3811 |
+ message="3분 안에는 되돌릴 수 있습니다." |
|
| 3812 |
+ actionLabel="실행 취소" |
|
| 3813 |
+ onAction={() => {}}
|
|
| 3814 |
+ onClose={() => {}}
|
|
| 3815 |
+ /> |
|
| 3816 |
+ <FoxSnackbar |
|
| 3817 |
+ title="파일 3개를 올렸습니다" |
|
| 3818 |
+ actionLabel="보기" |
|
| 3819 |
+ onAction={() => {}}
|
|
| 3820 |
+ onClose={() => {}}
|
|
| 3821 |
+ /> |
|
| 3822 |
+ <FoxSnackbar message="제목 없이 본문만 있는 스낵바입니다." onClose={() => {}} />
|
|
| 3823 |
+ </FoxSnackbarList> |
|
| 3824 |
+ ), |
|
| 3825 |
+ }, |
|
| 3826 |
+ {
|
|
| 3827 |
+ label: "하나만 있어도 그대로 씁니다", |
|
| 3828 |
+ node: ( |
|
| 3829 |
+ <FoxSnackbarList label="알림"> |
|
| 3830 |
+ <FoxSnackbar title="하나만 떠 있는 경우" onClose={() => {}} />
|
|
| 3831 |
+ </FoxSnackbarList> |
|
| 3832 |
+ ), |
|
| 3833 |
+ }, |
|
| 3834 |
+ ], |
|
| 3835 |
+ }, |
|
| 3558 | 3836 |
]; |
--- @fox/styles/_form-field.scss
+++ @fox/styles/_form-field.scss
... | ... | @@ -27,8 +27,14 @@ |
| 27 | 27 |
} |
| 28 | 28 |
|
| 29 | 29 |
/// 시안 helper-message. 오류·포커스 색은 상태 규칙이 덮는다. |
| 30 |
-@mixin message {
|
|
| 31 |
- display: flex; |
|
| 30 |
+/// |
|
| 31 |
+/// `$display`는 폼 안에서 `flex`이고, 단독으로 쓰는 `FoxHelperText`에서만 |
|
| 32 |
+/// `inline-flex`다 — 그쪽 시안이 내용만큼만 차지하게 준다. |
|
| 33 |
+/// |
|
| 34 |
+/// TODO(폼 통합, 적용 후 이 문단 삭제): 이 믹스인을 부르는 폼 다섯 곳은 앞으로 |
|
| 35 |
+/// `FoxHelperText`(`.fox-helper-text`)로 옮긴다 — 절차는 `_fox-helper-text.scss` 상단 참고. |
|
| 36 |
+@mixin message($display: flex) {
|
|
| 37 |
+ display: $display; |
|
| 32 | 38 |
gap: fox.number(3); |
| 33 | 39 |
align-items: flex-start; |
| 34 | 40 |
margin: 0; |
+++ @fox/styles/_fox-alert.scss
... | ... | @@ -0,0 +1,166 @@ |
| 1 | +// FoxAlert — 시안: 통합관리자페이지 디자인시스템 Figma alert | |
| 2 | +// | |
| 3 | +// 화면 안에 놓이는 띠 모양 알림이다. 아이콘 상자 · 제목 · 본문이 한 줄로 선다. | |
| 4 | +// **모달이 아니다** — 화면을 덮지 않고 내용 흐름 안에 그대로 놓인다. | |
| 5 | +// | |
| 6 | +// 마크업 계약 (React 밖 소비자용): | |
| 7 | +// <div class="fox-alert fox-alert--default" role="status"> | |
| 8 | +// <span class="fox-alert__icon">…아이콘…</span> | |
| 9 | +// <div class="fox-alert__content"> | |
| 10 | +// <strong class="fox-alert__title">제목</strong> | |
| 11 | +// <span class="fox-alert__message">본문</span> | |
| 12 | +// </div> | |
| 13 | +// </div> | |
| 14 | +// | |
| 15 | +// 제목·본문은 각각 없을 수 있다. 아이콘 상자는 아이콘이 없어도 그린다 — 시안이 흰 판을 | |
| 16 | +// 배치의 일부로 주고, 있고 없고에 따라 글자 시작점이 흔들리면 안 된다. | |
| 17 | +// | |
| 18 | +// 제목이 `heading/xsm`(title 패밀리·bold)이고 본문이 `body/md`(body 패밀리·regular)다. | |
| 19 | +// 글자 크기는 둘 다 15px이라 굵기와 패밀리로만 갈린다 — 시안이 그렇게 준다. | |
| 20 | +// | |
| 21 | +// 토큰이 아닌 값: | |
| 22 | +// - ⚠️ `inline-size: 72rem`: **시안의 폭(720px)을 일단 그대로 적용해 둔 것이다.** Figma가 | |
| 23 | +// 변수로 export하지 않았고 토큰 스케일에도 720이 없다(modal 계열의 가장 큰 값이 760px). | |
| 24 | +// 토큰이 생기면 `fox.*()`로 바꾸고, 폭을 고정하지 않는 쪽으로 정해지면 여기서 뺀다. | |
| 25 | +// 그때까지 폭을 바꿔야 하는 자리는 호출부가 `className`으로 덮는다 | |
| 26 | +// (`fox-toggle-quantity`와 같은 처리). | |
| 27 | +// - `max-inline-size: 100%`: 시안에 없는 구조 규칙이다. 720px보다 좁은 자리에 놓였을 때 | |
| 28 | +// 띠가 부모를 뚫고 나가지 않게 막는다 — 고정 폭만 두면 표 안이나 좁은 단에서 넘친다. | |
| 29 | +// - line-height / letter-spacing: 시안 텍스트 스타일 heading/xsm·body/md의 값이나 Figma가 | |
| 30 | +// 변수로 export하지 않는다. 시안의 -0.375px은 15px 기준이라 `-0.025em`으로 적는다 | |
| 31 | +// (다른 컴포넌트와 같은 근거). | |
| 32 | +// - box-sizing / display / flex / aspect-ratio: 구조 규칙. | |
| 33 | +// | |
| 34 | +// 아이콘은 컴포넌트의 `TYPE_ICON`이 계열마다 하나씩 쥐고 있다(Prohibit · Warning · | |
| 35 | +// CheckCircle · Info · ChatDots, 전부 `weight="duotone"`). 색은 **제목 글자와 같은 계열색**을 | |
| 36 | +// 쓴다(default는 `font-neutral-strong`) — 시안 확인 완료. | |
| 37 | +// | |
| 38 | +// duotone은 연한 면과 진한 선을 겹쳐 그리고 **둘 다 `currentColor`를 쓴다** — 연한 쪽은 | |
| 39 | +// 글리프 자체가 `opacity: 0.2`를 물고 있어 색을 하나만 줘도 두 톤이 나온다. | |
| 40 | +// | |
| 41 | +// duotone은 연한 면과 진한 선을 겹쳐 그리고 **둘 다 `currentColor`를 쓴다** — 연한 쪽은 | |
| 42 | +// 글리프 자체가 `opacity: 0.2`를 물고 있어 색을 하나만 줘도 두 톤이 나온다. | |
| 43 | +// | |
| 44 | + | |
| 45 | +@use "@fox/styles/abstracts" as fox; | |
| 46 | + | |
| 47 | +.fox-alert { | |
| 48 | + // 안쪽 여백이 바깥 폭을 늘리지 않게 한다 — 720px이 720px로 그려진다. | |
| 49 | + box-sizing: border-box; | |
| 50 | + display: flex; | |
| 51 | + align-items: center; | |
| 52 | + gap: fox.gap(4); | |
| 53 | + inline-size: 72rem; | |
| 54 | + max-inline-size: 100%; | |
| 55 | + padding: fox.padding(5) fox.padding(6); | |
| 56 | + border: fox.border(1) solid fox.color(border-neutral-subtler); | |
| 57 | + border-radius: fox.radius(4); | |
| 58 | + background: fox.color(surface-neutral-gray); | |
| 59 | + | |
| 60 | + // 정사각 흰 판. 글자가 길어져 띠가 늘어나도 크기가 그대로여야 해서 줄어들지 않게 막는다. | |
| 61 | + &__icon { | |
| 62 | + display: flex; | |
| 63 | + flex-shrink: 0; | |
| 64 | + justify-content: center; | |
| 65 | + align-items: center; | |
| 66 | + inline-size: fox.number(13); | |
| 67 | + block-size: fox.number(13); | |
| 68 | + aspect-ratio: 1 / 1; | |
| 69 | + border-radius: fox.radius(4); | |
| 70 | + background: fox.color(element-neutral-inverse); | |
| 71 | + // 글리프가 `fill: currentColor`라 이 색을 그대로 입는다. 계열이 있으면 아래에서 덮는다. | |
| 72 | + color: fox.color(font-neutral-strong); | |
| 73 | + } | |
| 74 | + | |
| 75 | + // 글리프는 흰 판(36px)보다 작은 24px이다 — 판이 글리프를 감싸는 여백을 갖는다. | |
| 76 | + // 상자와 마찬가지로 줄어들지 않게 막고 정사각을 못박는다(시안이 둘 다 명시한다). | |
| 77 | + &__icon > * { | |
| 78 | + flex-shrink: 0; | |
| 79 | + inline-size: fox.icon(5); | |
| 80 | + block-size: fox.icon(5); | |
| 81 | + aspect-ratio: 1 / 1; | |
| 82 | + } | |
| 83 | + | |
| 84 | + // 남는 폭을 전부 가져간다. `min-inline-size: 0`이 없으면 긴 글이 줄바꿈되지 않고 띠를 | |
| 85 | + // 밀어 넓힌다(flex 자식의 기본 최소 크기가 내용 크기라서다). | |
| 86 | + &__content { | |
| 87 | + display: flex; | |
| 88 | + flex: 1 0 0; | |
| 89 | + flex-direction: column; | |
| 90 | + align-items: flex-start; | |
| 91 | + gap: fox.gap(1); | |
| 92 | + min-inline-size: 0; | |
| 93 | + } | |
| 94 | + | |
| 95 | + &__title { | |
| 96 | + align-self: stretch; | |
| 97 | + color: fox.color(font-neutral-strong); | |
| 98 | + font-family: fox.font-family(title); | |
| 99 | + font-size: fox.font-size(heading-xsm); | |
| 100 | + font-weight: fox.font-weight(bold); | |
| 101 | + line-height: 1.4; | |
| 102 | + letter-spacing: -0.025em; | |
| 103 | + } | |
| 104 | + | |
| 105 | + &__message { | |
| 106 | + align-self: stretch; | |
| 107 | + color: fox.color(font-neutral-strong); | |
| 108 | + font-family: fox.font-family(body); | |
| 109 | + font-size: fox.font-size(body-md); | |
| 110 | + font-weight: fox.font-weight(regular); | |
| 111 | + line-height: 1.5; | |
| 112 | + letter-spacing: -0.025em; | |
| 113 | + } | |
| 114 | + | |
| 115 | + // ── 계열 ──────────────────────────────────────────────────────────────────── | |
| 116 | + // 계열이 바꾸는 것은 **배경 · 테두리 · 제목 글자색** 셋뿐이다. 본문 글자색과 아이콘 상자의 | |
| 117 | + // 흰 배경은 계열과 무관하게 블록 규칙을 그대로 쓴다(시안 확인). | |
| 118 | + // | |
| 119 | + // 테두리만 `-inverse`다 — 같은 계열의 5(연한 배경)와 10(그보다 한 단계 진한 선)을 짝지어 | |
| 120 | + // 쓰라고 토큰이 그렇게 갈라져 있다. 다크 모드에서는 세 값이 함께 뒤집힌다(95 / 90 / 20). | |
| 121 | + // | |
| 122 | + // `default`는 블록 규칙이 곧 그 계열의 모양이라 따로 덮을 것이 없다. | |
| 123 | + &--default { | |
| 124 | + // 시안 그대로 — 블록에 있는 값이 전부다. | |
| 125 | + } | |
| 126 | + | |
| 127 | + &--information { | |
| 128 | + border-color: fox.color(border-system-information-inverse); | |
| 129 | + background: fox.color(surface-system-information); | |
| 130 | + } | |
| 131 | + | |
| 132 | + &--information &__title, | |
| 133 | + &--information &__icon { | |
| 134 | + color: fox.color(font-system-information); | |
| 135 | + } | |
| 136 | + | |
| 137 | + &--success { | |
| 138 | + border-color: fox.color(border-system-success-inverse); | |
| 139 | + background: fox.color(surface-system-success); | |
| 140 | + } | |
| 141 | + | |
| 142 | + &--success &__title, | |
| 143 | + &--success &__icon { | |
| 144 | + color: fox.color(font-system-success); | |
| 145 | + } | |
| 146 | + | |
| 147 | + &--warning { | |
| 148 | + border-color: fox.color(border-system-warning-inverse); | |
| 149 | + background: fox.color(surface-system-warning); | |
| 150 | + } | |
| 151 | + | |
| 152 | + &--warning &__title, | |
| 153 | + &--warning &__icon { | |
| 154 | + color: fox.color(font-system-warning); | |
| 155 | + } | |
| 156 | + | |
| 157 | + &--danger { | |
| 158 | + border-color: fox.color(border-system-danger-inverse); | |
| 159 | + background: fox.color(surface-system-danger); | |
| 160 | + } | |
| 161 | + | |
| 162 | + &--danger &__title, | |
| 163 | + &--danger &__icon { | |
| 164 | + color: fox.color(font-system-danger); | |
| 165 | + } | |
| 166 | +} |
+++ @fox/styles/_fox-helper-text.scss
... | ... | @@ -0,0 +1,105 @@ |
| 1 | +// FoxHelperText — 시안: 통합관리자페이지 디자인시스템 Figma helper-txt | |
| 2 | +// | |
| 3 | +// 입력 아래 한 줄로 붙는 안내·오류 문구다. 아이콘 하나와 글자 한 덩어리로 끝난다. | |
| 4 | +// | |
| 5 | +// 마크업 계약 (React 밖 소비자용): | |
| 6 | +// <p class="fox-helper-text fox-helper-text--danger"> | |
| 7 | +// <span class="fox-helper-text__icon">…아이콘 svg…</span> | |
| 8 | +// <span class="fox-helper-text__message">필수 항목입니다.</span> | |
| 9 | +// </p> | |
| 10 | +// | |
| 11 | +// 아이콘은 없어도 된다(글자만 있는 자리가 많다). 계열은 `FoxAlert`와 같은 다섯 개이고, | |
| 12 | +// 글리프도 같은 것을 쓰되 **굵기만 regular**다(알럿은 duotone). | |
| 13 | +// | |
| 14 | +// **모양 값을 여기서 새로 적지 않는다.** 시안 값이 `_form-field.scss`의 `message`· | |
| 15 | +// `message-icon` 믹스인과 한 자도 다르지 않아(gap 4px · 아이콘 padding-top 2px · body/sm · | |
| 16 | +// line-height 150% · letter-spacing -0.025em) 그대로 부른다. 폼 컴포넌트의 `__message`가 | |
| 17 | +// 같은 믹스인을 쓰므로, 두 경로가 갈라질 수 없다. | |
| 18 | +// | |
| 19 | +// 유일한 차이가 `display`다 — 시안이 `inline-flex`를 준다(폼 안에서는 `flex`). 문구가 | |
| 20 | +// 글줄 안에 놓일 수 있어서 폭을 내용만큼만 갖는 쪽이 맞다. | |
| 21 | +// | |
| 22 | +// 계열은 **글자와 아이콘의 색만** 바꾼다. `default`는 믹스인이 정한 회색 | |
| 23 | +// (`font-neutral-subtle`)이고, 나머지 넷은 알럿 제목과 같은 `font-system-*`이다. 시안 확인 완료. | |
| 24 | +// | |
| 25 | +// 아이콘 크기도 믹스인이 정한 16px(`icon-3`)이 시안 값이다 — 폼 안 헬퍼와 같다(시안 확인 완료). | |
| 26 | +// | |
| 27 | +// TODO(폼 통합, 적용 후 이 주석 삭제): 폼 컴포넌트 여섯 곳의 헬퍼를 이 클래스로 모은다. | |
| 28 | +// 지금은 각자 그린다 — FoxInput · FoxTextArea · FoxEmail · FoxPhoneNumber · FoxAddress가 | |
| 29 | +// `__message`, FoxSelect가 `__hint`다. 모양은 `_form-field.scss`의 `message` 믹스인 한 | |
| 30 | +// 곳에서 나와 어긋나지는 않지만 마크업이 여섯 벌이다. | |
| 31 | +// | |
| 32 | +// 옮길 때 할 일: | |
| 33 | +// 1) `FoxHelperText`에 `icon` 오버라이드 prop을 더한다 — 폼은 호출부가 준 아이콘을 쓰고 | |
| 34 | +// 계열 글리프를 쓰지 않는다. | |
| 35 | +// 2) tsx의 `<p class="fox-X__message">`를 `<FoxHelperText className="fox-X__message" …>`로 | |
| 36 | +// 바꾼다. **클래스를 남기는 것이 요점이다** — 그래야 상태 색 규칙(`auto-states`의 | |
| 37 | +// `:focus-within`·`:has([aria-invalid])`)이 선택자를 고치지 않고 그대로 먹는다. | |
| 38 | +// 3) 각 컴포넌트 scss에서 `&__message { @include field.message; }`와 | |
| 39 | +// `&__message-icon { … }` 블록을 지운다 — `.fox-helper-text`가 대신 준다. | |
| 40 | +// 4) 아이콘은 `showIcon={Boolean(messageIcon)}`으로 넘겨 기존 동작(아이콘을 안 주면 안 | |
| 41 | +// 그린다)을 유지한다. | |
| 42 | +// | |
| 43 | +// ⚠️ 옮기면 **FoxSelect만 모양이 달라진다.** 진행 전에 확인받을 것: | |
| 44 | +// - 힌트 간격이 `gap 8px` · `align-items: center`에서 `4px` · `flex-start`로 바뀐다 | |
| 45 | +// (helper-txt 시안 값이 이쪽이다). | |
| 46 | +// - 오류일 때 아이콘 색이 `icon-system-danger`(danger-50)에서, 글자색을 상속한 | |
| 47 | +// `font-system-danger`(danger-60)로 한 단계 진해진다. | |
| 48 | +// | |
| 49 | +// 나머지 다섯은 값이 같아 화면이 바뀌지 않는다. 앱·다른 컴포넌트가 `__message`·`__hint` | |
| 50 | +// 클래스를 직접 참조하는 곳은 없다(확인 완료). | |
| 51 | + | |
| 52 | +@use "@fox/styles/abstracts" as fox; | |
| 53 | +@use "@fox/styles/form-field" as field; | |
| 54 | + | |
| 55 | +.fox-helper-text { | |
| 56 | + // 시안이 inline-flex다(폼 안에서는 flex) — 믹스인에 인자로 넘겨 한 번만 적는다. | |
| 57 | + @include field.message($display: inline-flex); | |
| 58 | + | |
| 59 | + &__icon { | |
| 60 | + @include field.message-icon; | |
| 61 | + } | |
| 62 | + | |
| 63 | + // ── 계열 ──────────────────────────────────────────────────────────────────── | |
| 64 | + // 글자와 아이콘의 색이 **다르다** — 글자는 `font-system-*`, 아이콘은 한 단계 진한 | |
| 65 | + // `icon-system-*-strong`이다(시안 확인). 아이콘 글리프가 `fill: currentColor`라 슬롯에 | |
| 66 | + // 색을 주면 그대로 입는다. | |
| 67 | + // | |
| 68 | + // `default`만 예외다 — 글자도 아이콘도 믹스인이 정한 회색(`font-neutral-subtle`)을 함께 | |
| 69 | + // 쓰므로 따로 덮을 것이 없다. | |
| 70 | + &--default { | |
| 71 | + // 시안 그대로 — 믹스인에 있는 값이 전부다. | |
| 72 | + } | |
| 73 | + | |
| 74 | + &--information { | |
| 75 | + color: fox.color(font-system-information); | |
| 76 | + } | |
| 77 | + | |
| 78 | + &--information &__icon { | |
| 79 | + color: fox.color(icon-system-information-strong); | |
| 80 | + } | |
| 81 | + | |
| 82 | + &--success { | |
| 83 | + color: fox.color(font-system-success); | |
| 84 | + } | |
| 85 | + | |
| 86 | + &--success &__icon { | |
| 87 | + color: fox.color(icon-system-success-strong); | |
| 88 | + } | |
| 89 | + | |
| 90 | + &--warning { | |
| 91 | + color: fox.color(font-system-warning); | |
| 92 | + } | |
| 93 | + | |
| 94 | + &--warning &__icon { | |
| 95 | + color: fox.color(icon-system-warning-strong); | |
| 96 | + } | |
| 97 | + | |
| 98 | + &--danger { | |
| 99 | + color: fox.color(font-system-danger); | |
| 100 | + } | |
| 101 | + | |
| 102 | + &--danger &__icon { | |
| 103 | + color: fox.color(icon-system-danger-strong); | |
| 104 | + } | |
| 105 | +} |
+++ @fox/styles/_fox-snackbar-list.scss
... | ... | @@ -0,0 +1,38 @@ |
| 1 | +// FoxSnackbarList — 시안: 통합관리자페이지 디자인시스템 Figma snackbar-list | |
| 2 | +// | |
| 3 | +// 스낵바가 여러 개일 때 세로로 쌓는 묶음이다. 배치만 갖는다 — 무엇이 몇 개 떠 있는지는 | |
| 4 | +// 호출부가 소유한다. | |
| 5 | +// | |
| 6 | +// 마크업 계약 (React 밖 소비자용): | |
| 7 | +// <div class="fox-snackbar-list" role="group"> | |
| 8 | +// …fox-snackbar… | |
| 9 | +// …fox-snackbar… | |
| 10 | +// </div> | |
| 11 | +// | |
| 12 | +// 자식 폭과 묶음 폭이 같은 640px이다 — 묶음이 자식을 늘이거나 줄이지 않고, `align-items`가 | |
| 13 | +// `flex-start`라 자식이 자기 폭을 그대로 갖는다(시안 지정). | |
| 14 | +// | |
| 15 | +// 토큰이 아닌 값: | |
| 16 | +// - ⚠️ `inline-size: 64rem`: **시안의 폭(640px)을 그대로 적용해 둔 것이다.** 자식 | |
| 17 | +// `fox-snackbar`와 같은 값이고 근거도 같다 — 토큰 스케일에 640이 없다. 폭을 바꿔야 하는 | |
| 18 | +// 자리는 호출부가 `className`으로 덮는다. | |
| 19 | +// - display / flex-direction / align-items: 구조 규칙. | |
| 20 | +// | |
| 21 | +// ⚠️ 모바일에서는 자식과 같은 이유로 고정 폭을 푼다 — 640px을 그대로 두면 화면 밖으로 넘쳐 | |
| 22 | +// 잘린다(`fox-snackbar` 상단 주석 참고). | |
| 23 | + | |
| 24 | +@use "@fox/styles/abstracts" as fox; | |
| 25 | + | |
| 26 | +.fox-snackbar-list { | |
| 27 | + box-sizing: border-box; | |
| 28 | + display: flex; | |
| 29 | + flex-direction: column; | |
| 30 | + align-items: flex-start; | |
| 31 | + gap: fox.gap(3); | |
| 32 | + inline-size: 64rem; | |
| 33 | + | |
| 34 | + // 모바일에서는 부모 폭을 채운다. 자식(`fox-snackbar`)도 같은 규칙을 스스로 갖는다. | |
| 35 | + @include fox.mobile { | |
| 36 | + inline-size: 100%; | |
| 37 | + } | |
| 38 | +} |
+++ @fox/styles/_fox-snackbar.scss
... | ... | @@ -0,0 +1,181 @@ |
| 1 | +// FoxSnackbar — 시안: 통합관리자페이지 디자인시스템 Figma snackbar | |
| 2 | +// | |
| 3 | +// 흰 판 위에 글자(제목·본문)와 글자 버튼이 서고, 오른쪽 위에 닫기가 붙는다. 제목과 본문은 | |
| 4 | +// 각각 없을 수 있다 — 한쪽만 있으면 그 줄만 그린다. `FoxToast`와 형제지만 | |
| 5 | +// **누를 것이 있다** — 그래서 스스로 사라지지 않고 손이 닿을 때까지 남는다. | |
| 6 | +// | |
| 7 | +// 마크업 계약 (React 밖 소비자용): | |
| 8 | +// <div class="fox-snackbar" role="status"> | |
| 9 | +// <span class="fox-snackbar__icon">…아이콘 svg…</span> <!-- 선택 --> | |
| 10 | +// <div class="fox-snackbar__content"> | |
| 11 | +// <strong class="fox-snackbar__title">저장했습니다</strong> <!-- 선택 --> | |
| 12 | +// <span class="fox-snackbar__message">3분 안에 되돌릴 수 있습니다.</span> <!-- 선택 --> | |
| 13 | +// </div> | |
| 14 | +// <button class="fox-text-button fox-text-button--primary fox-text-button--md | |
| 15 | +// fox-snackbar__action">실행 취소</button> <!-- 선택 --> | |
| 16 | +// <button class="fox-snackbar__close" aria-label="닫기">…X svg…</button> <!-- 선택 --> | |
| 17 | +// </div> | |
| 18 | +// | |
| 19 | +// 글자 버튼은 `FoxTextButton`의 primary·md를 그대로 쓴다 — 스낵바가 버튼 모양을 다시 정하지 | |
| 20 | +// 않고 `fox-snackbar__action`은 배치만 맡는다. | |
| 21 | +// | |
| 22 | +// 토큰이 아닌 값: | |
| 23 | +// - ⚠️ `inline-size` / `min-inline-size: 64rem`: **시안의 폭(640px)을 그대로 적용해 둔 | |
| 24 | +// 것이다.** Figma가 변수로 export하지 않았고 토큰 스케일에도 640이 없다. 폭을 바꿔야 하는 | |
| 25 | +// 자리는 호출부가 `className`으로 덮는다(`fox-toggle-quantity`·`fox-alert`와 같은 처리). | |
| 26 | +// - ⚠️ `padding-inline-end`가 64px이라 토큰이 없다. 닫기가 `right: 0`으로 오른쪽 끝에 | |
| 27 | +// 붙으므로(시안 지정) 이 여백은 **글자·버튼이 닫기 밑으로 들어가지 않게 비워 두는 | |
| 28 | +// 자리**다. 20px + 아이콘 24px + 20px = 64px로 떨어져서 그 조합으로 적는다 | |
| 29 | +// (`padding(7)` + `icon(5)` + `padding(7)`). | |
| 30 | +// - line-height / letter-spacing: 시안 텍스트 스타일 heading/xsm의 값이나 Figma가 변수로 | |
| 31 | +// export하지 않는다. -0.375px은 15px 기준이라 `-0.025em`으로 적는다(다른 컴포넌트와 같은 | |
| 32 | +// 근거). | |
| 33 | +// - box-sizing / position / display / flex: 구조 규칙. | |
| 34 | +// | |
| 35 | +// ⚠️ 그림자 색: 시안이 두 겹을 각각 `dark/alpha/black10`·`alpha/black10`으로 물려 놓았는데 | |
| 36 | +// 값이 같고(rgba(0,0,0,0.10)) 앞쪽 이름의 토큰은 export에 없다. 두 겹 모두 `alpha-black10`을 | |
| 37 | +// 쓴다 — `fox-toast`·`fox-select-option`과 같은 선택이다. | |
| 38 | +// | |
| 39 | +// ⚠️ 폭 세 값(640 / 640 / `grid(wrap-sm)`)은 **PC 기준이다.** 모바일에서 `grid(wrap-sm)`은 | |
| 40 | +// 320px인데 최소 폭 640px이 그것을 이겨 화면 밖으로 넘친다 — 실제로 잘렸다. 그래서 모바일에서는 | |
| 41 | +// 최소 폭을 풀고 판이 부모 폭을 채우게 둔다(아래 `fox.mobile` 블록). | |
| 42 | +// | |
| 43 | +// 시안이 모바일 스낵바의 폭을 따로 주면 그 값으로 바꾼다. 지금은 "화면을 넘지 않는다"만 | |
| 44 | +// 보장한다. | |
| 45 | + | |
| 46 | +@use "@fox/styles/abstracts" as fox; | |
| 47 | + | |
| 48 | +.fox-snackbar { | |
| 49 | + // 안쪽 여백이 바깥 폭을 늘리지 않게 한다 — 640px이 640px로 그려진다. | |
| 50 | + box-sizing: border-box; | |
| 51 | + // 닫기 버튼의 위치 기준이 된다. | |
| 52 | + position: relative; | |
| 53 | + display: flex; | |
| 54 | + align-items: center; | |
| 55 | + gap: fox.gap(5); | |
| 56 | + inline-size: 64rem; | |
| 57 | + min-inline-size: 64rem; | |
| 58 | + max-inline-size: fox.grid(wrap-sm); | |
| 59 | + padding-block: fox.padding(6); | |
| 60 | + padding-inline-start: fox.padding(7); | |
| 61 | + // 닫기 버튼이 앉을 자리(위 주석 참고). | |
| 62 | + padding-inline-end: calc(#{fox.padding(7)} + #{fox.icon(5)} + #{fox.padding(7)}); | |
| 63 | + border: fox.border(1) solid fox.color(border-neutral-subtle); | |
| 64 | + border-radius: fox.radius(5); | |
| 65 | + background: fox.color(surface-neutral-default); | |
| 66 | + | |
| 67 | + // 시안의 neutral/lv2 — 가까운 그림자와 먼 그림자 두 겹이다. | |
| 68 | + box-shadow: | |
| 69 | + fox.shadow(location1) fox.shadow(location1) fox.shadow(blur2) 0 | |
| 70 | + fox.primitive(alpha-black10), | |
| 71 | + fox.shadow(location3) fox.shadow(location3) fox.shadow(blur3) 0 | |
| 72 | + fox.primitive(alpha-black10); | |
| 73 | + | |
| 74 | + // 모바일에서는 640px 고정을 풀고 부모 폭을 채운다(위 주석 참고). `min-inline-size`를 함께 | |
| 75 | + // 풀지 않으면 640px이 그대로 남아 화면 밖으로 밀려 잘린다. | |
| 76 | + @include fox.mobile { | |
| 77 | + inline-size: 100%; | |
| 78 | + min-inline-size: 0; | |
| 79 | + max-inline-size: 100%; | |
| 80 | + } | |
| 81 | + | |
| 82 | + // 글자 앞 아이콘. 글자가 길어 판이 높아져도 크기가 그대로여야 해서 줄어들지 않게 막는다. | |
| 83 | + // | |
| 84 | + // 색은 `icon-theme-primary`다(시안 확인). 글리프가 `fill: currentColor`라 슬롯에 색만 | |
| 85 | + // 주면 그대로 입는다 — 굵기는 호출부가 넘기는 글리프가 정하고 시안은 `fill`이다. | |
| 86 | + // | |
| 87 | + // 크기는 시안 값 그대로 24px(`icon(5)`)이고 닫기 아이콘과 같다. 세로 정렬은 시안에 없어 | |
| 88 | + // 판 규칙(가운데)을 그대로 따른다. | |
| 89 | + &__icon { | |
| 90 | + display: inline-flex; | |
| 91 | + flex-shrink: 0; | |
| 92 | + align-items: center; | |
| 93 | + justify-content: center; | |
| 94 | + inline-size: fox.icon(5); | |
| 95 | + block-size: fox.icon(5); | |
| 96 | + aspect-ratio: 1 / 1; | |
| 97 | + color: fox.color(icon-theme-primary); | |
| 98 | + } | |
| 99 | + | |
| 100 | + // 넘긴 SVG가 슬롯을 꽉 채운다 — 크기는 슬롯이 정하고 SVG는 따라온다. | |
| 101 | + &__icon > * { | |
| 102 | + inline-size: 100%; | |
| 103 | + block-size: 100%; | |
| 104 | + } | |
| 105 | + | |
| 106 | + // 남는 폭을 전부 가져가 글자 버튼을 오른쪽 끝으로 민다. `min-inline-size: 0`이 없으면 | |
| 107 | + // 긴 글이 줄바꿈되지 않고 판을 밀어 넓힌다(flex 자식의 기본 최소 크기가 내용 크기라서다). | |
| 108 | + // | |
| 109 | + // `flex: 1 0 0`으로 남는 폭을 전부 가져가 글자 버튼을 오른쪽 끝으로 민다. 제목과 본문 | |
| 110 | + // 사이는 `gap(1)`(2px)이고 둘 다 왼쪽에 붙는다 — 시안 확인 완료(`FoxAlert`의 같은 자리와 | |
| 111 | + // 같은 값이다). | |
| 112 | + &__content { | |
| 113 | + display: flex; | |
| 114 | + flex: 1 0 0; | |
| 115 | + flex-direction: column; | |
| 116 | + align-items: flex-start; | |
| 117 | + gap: fox.gap(1); | |
| 118 | + min-inline-size: 0; | |
| 119 | + } | |
| 120 | + | |
| 121 | + &__title { | |
| 122 | + align-self: stretch; | |
| 123 | + color: fox.color(font-neutral-strong); | |
| 124 | + font-family: fox.font-family(title); | |
| 125 | + font-size: fox.font-size(heading-xsm); | |
| 126 | + font-weight: fox.font-weight(bold); | |
| 127 | + line-height: 1.4; | |
| 128 | + letter-spacing: -0.025em; | |
| 129 | + } | |
| 130 | + | |
| 131 | + &__message { | |
| 132 | + align-self: stretch; | |
| 133 | + color: fox.color(font-neutral-strong); | |
| 134 | + font-family: fox.font-family(body); | |
| 135 | + font-size: fox.font-size(body-md); | |
| 136 | + font-weight: fox.font-weight(regular); | |
| 137 | + line-height: 1.5; | |
| 138 | + letter-spacing: -0.025em; | |
| 139 | + } | |
| 140 | + | |
| 141 | + // 글자 버튼은 자기 모양을 그대로 갖는다 — 여기서는 줄어들지만 않게 막는다. | |
| 142 | + &__action { | |
| 143 | + flex-shrink: 0; | |
| 144 | + } | |
| 145 | + | |
| 146 | + // 오른쪽 끝에 붙는다(시안 지정 `right: 0`). 흐름에서 빠져 있어 글자·버튼 배치에 끼어들지 | |
| 147 | + // 않고, 판의 오른쪽 여백(64px)이 그 아래로 글자가 들어가지 않게 막아 준다. | |
| 148 | + // | |
| 149 | + // 누르는 자리는 48px 정사각이고 아이콘은 24px이다 — 손가락이 닿는 넓이를 아이콘보다 크게 | |
| 150 | + // 잡은 것이라, 여백을 줄이면 모바일에서 누르기 어려워진다. | |
| 151 | + // | |
| 152 | + // ⚠️ 시안이 이 칸에 `align-items: flex-end`를 준다. 가로로는 시작(왼쪽), 세로로는 끝(아래)에 | |
| 153 | + // 붙어서 **아이콘이 48px 칸의 왼쪽 아래로 치우친다** — 오른쪽 끝에서 16px, 아래에서 8px이다. | |
| 154 | + // 시안 값을 그대로 옮긴 결과이니, 화면에서 가운데로 보여야 하면 `center`로 바꾼다. | |
| 155 | + // | |
| 156 | + // 세로는 판 위쪽에 붙는다(시안 지정 `top: 0`). 가운데 정렬이 아니라서, 본문이 늘어 판이 | |
| 157 | + // 높아져도 닫기는 첫 줄 옆에 그대로 남는다. | |
| 158 | + &__close { | |
| 159 | + position: absolute; | |
| 160 | + inset-inline-end: 0; | |
| 161 | + inset-block-start: 0; | |
| 162 | + display: flex; | |
| 163 | + align-items: flex-end; | |
| 164 | + inline-size: fox.number(16); | |
| 165 | + block-size: fox.number(16); | |
| 166 | + aspect-ratio: 1 / 1; | |
| 167 | + padding: fox.padding(3); | |
| 168 | + border: none; | |
| 169 | + background: none; | |
| 170 | + color: fox.color(icon-neutral-default); | |
| 171 | + cursor: pointer; | |
| 172 | + } | |
| 173 | + | |
| 174 | + // 아이콘은 24px 정사각으로 못박는다 — 칸이 48px이라 늘어나게 두면 안 된다. | |
| 175 | + &__close > * { | |
| 176 | + flex-shrink: 0; | |
| 177 | + inline-size: fox.icon(5); | |
| 178 | + block-size: fox.icon(5); | |
| 179 | + aspect-ratio: 1 / 1; | |
| 180 | + } | |
| 181 | +} |
+++ @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/_fox-toast.scss
... | ... | @@ -0,0 +1,53 @@ |
| 1 | +// FoxToast — 시안: 통합관리자페이지 디자인시스템 Figma toast | |
| 2 | +// | |
| 3 | +// 어두운 판 위에 한 줄짜리 알림을 얹는다. 크기 수식어가 없다 — 시안에 하나뿐이다. | |
| 4 | +// | |
| 5 | +// 마크업 계약 (React 밖 소비자용): | |
| 6 | +// <div class="fox-toast" role="status">저장되었습니다.</div> | |
| 7 | +// | |
| 8 | +// 글자를 감싸는 엘리먼트를 두지 않는다 — 안에 들어가는 것이 글자 하나뿐이라 따로 규칙을 | |
| 9 | +// 걸 대상이 없다. 아이콘이나 버튼이 붙는 날이 오면 그때 `__label`을 만든다. | |
| 10 | +// | |
| 11 | +// **띄우는 위치는 여기서 정하지 않는다.** 시안이 조각의 모양만 주고, 화면 어디에 어떻게 | |
| 12 | +// 나타나는지는 쓰는 화면이 정한다(사용자 결정). 호출부가 `className`으로 `position: fixed` | |
| 13 | +// 같은 배치를 준다. | |
| 14 | +// | |
| 15 | +// 배경·글자색이 `*-inverse` 토큰이다 — 라이트에서 어두운 판 + 흰 글자이고, 다크 모드에서는 | |
| 16 | +// 둘이 함께 뒤집혀 밝은 판 + 어두운 글자가 된다. 두 값을 한 쌍으로 골라야 대비가 유지된다. | |
| 17 | +// | |
| 18 | +// 토큰이 아닌 값(FoxButton·FoxTag와 동일한 근거로 승인된 항목): | |
| 19 | +// - line-height / letter-spacing: 시안 텍스트 스타일 body/md의 값이나 Figma가 변수로 | |
| 20 | +// export하지 않는다. 시안의 -0.375px은 15px 기준이라 `-0.025em`으로 적는다 — px으로 | |
| 21 | +// 박으면 글자 크기가 모바일에서 13px으로 줄 때 비율이 어긋난다. | |
| 22 | +// - box-sizing / display / align-items: 구조 규칙. | |
| 23 | +// | |
| 24 | +// ⚠️ 그림자 색: 시안이 두 겹을 각각 `dark/alpha/black10`·`alpha/black10`으로 물려 놓았는데 | |
| 25 | +// 값이 같고(rgba(0,0,0,0.10)) 앞쪽 이름의 토큰은 export에 없다. 그래서 두 겹 모두 | |
| 26 | +// `alpha-black10`을 쓴다 — `fox-select-option`과 같은 선택이다(다크 모드에서도 검은 그림자). | |
| 27 | + | |
| 28 | +@use "@fox/styles/abstracts" as fox; | |
| 29 | + | |
| 30 | +.fox-toast { | |
| 31 | + // 안쪽 여백이 바깥 크기를 늘리지 않게 한다. | |
| 32 | + box-sizing: border-box; | |
| 33 | + display: inline-flex; | |
| 34 | + // 글자 하나뿐이라 지금은 티가 나지 않지만, 시안이 세로 가운데 정렬을 명시한다. | |
| 35 | + align-items: center; | |
| 36 | + padding: fox.padding(5) fox.padding(7); | |
| 37 | + border-radius: fox.radius(4); | |
| 38 | + background: fox.color(surface-neutral-default-inverse); | |
| 39 | + | |
| 40 | + // 시안의 neutral/lv3 — 가까운 그림자와 먼 그림자 두 겹이다. | |
| 41 | + box-shadow: | |
| 42 | + fox.shadow(location2) fox.shadow(location2) fox.shadow(blur3) 0 | |
| 43 | + fox.primitive(alpha-black10), | |
| 44 | + fox.shadow(location4) fox.shadow(location4) fox.shadow(blur4) 0 | |
| 45 | + fox.primitive(alpha-black10); | |
| 46 | + | |
| 47 | + color: fox.color(font-neutral-strong-inverse); | |
| 48 | + font-family: fox.font-family(body); | |
| 49 | + font-size: fox.font-size(body-md); | |
| 50 | + font-weight: fox.font-weight(regular); | |
| 51 | + line-height: 1.5; | |
| 52 | + letter-spacing: -0.025em; | |
| 53 | +} |
--- @fox/styles/components.scss
+++ @fox/styles/components.scss
... | ... | @@ -8,6 +8,7 @@ |
| 8 | 8 |
@use "fox-button-group"; |
| 9 | 9 |
@use "fox-button-panel"; |
| 10 | 10 |
@use "fox-address"; |
| 11 |
+@use "fox-alert"; |
|
| 11 | 12 |
@use "fox-certified"; |
| 12 | 13 |
@use "fox-check-icon"; |
| 13 | 14 |
@use "fox-checkbox"; |
... | ... | @@ -19,6 +20,7 @@ |
| 19 | 20 |
@use "fox-email"; |
| 20 | 21 |
@use "fox-file-upload"; |
| 21 | 22 |
@use "fox-form-label"; |
| 23 |
+@use "fox-helper-text"; |
|
| 22 | 24 |
@use "fox-input"; |
| 23 | 25 |
@use "fox-phone-number"; |
| 24 | 26 |
@use "fox-radio"; |
... | ... | @@ -49,7 +51,11 @@ |
| 49 | 51 |
@use "fox-table-cell"; |
| 50 | 52 |
@use "fox-tab"; |
| 51 | 53 |
@use "fox-tab-item"; |
| 54 |
+@use "fox-snackbar"; |
|
| 55 |
+@use "fox-snackbar-list"; |
|
| 52 | 56 |
@use "fox-spinner"; |
| 57 |
+@use "fox-status-indicator"; |
|
| 58 |
+@use "fox-toast"; |
|
| 53 | 59 |
@use "fox-toggle-quantity"; |
| 54 | 60 |
@use "fox-page-header"; |
| 55 | 61 |
@use "fox-modal"; |
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?