+++ @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/index.ts
+++ @fox/core/components/index.ts
... | ... | @@ -23,6 +23,7 @@ |
| 23 | 23 |
export * from "./fox-email"; |
| 24 | 24 |
export * from "./fox-file-upload"; |
| 25 | 25 |
export * from "./fox-form-label"; |
| 26 |
+export * from "./fox-helper-text"; |
|
| 26 | 27 |
export * from "./fox-icon-button"; |
| 27 | 28 |
export * from "./fox-input"; |
| 28 | 29 |
export * from "./fox-link-button"; |
--- @fox/dev-test/component-registry.tsx
+++ @fox/dev-test/component-registry.tsx
... | ... | @@ -19,6 +19,7 @@ |
| 19 | 19 |
import { FoxEmail } from "../core/components/fox-email";
|
| 20 | 20 |
import { FoxFileUpload, type FoxFileItem } from "../core/components/fox-file-upload";
|
| 21 | 21 |
import { FoxFormLabel } from "../core/components/fox-form-label";
|
| 22 |
+import { FoxHelperText } from "../core/components/fox-helper-text";
|
|
| 22 | 23 |
import { FoxIconButton } from "../core/components/fox-icon-button";
|
| 23 | 24 |
import { FoxLinkButton } from "../core/components/fox-link-button";
|
| 24 | 25 |
import { FoxPagination } from "../core/components/fox-pagination";
|
... | ... | @@ -3369,4 +3370,52 @@ |
| 3369 | 3370 |
}, |
| 3370 | 3371 |
], |
| 3371 | 3372 |
}, |
| 3373 |
+ {
|
|
| 3374 |
+ id: "fox-helper-text", |
|
| 3375 |
+ name: "FoxHelperText", |
|
| 3376 |
+ description: |
|
| 3377 |
+ "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 믹스인). 모으는 것은 별도 작업입니다.", |
|
| 3378 |
+ variants: [ |
|
| 3379 |
+ {
|
|
| 3380 |
+ label: "계열 5종 — 아이콘이 글자보다 한 단계 진합니다 (icon-system-*-strong)", |
|
| 3381 |
+ node: ( |
|
| 3382 |
+ <> |
|
| 3383 |
+ <FoxHelperText message="기본 안내 문구입니다." /> |
|
| 3384 |
+ <FoxHelperText type="information" message="입력 형식을 확인해 주세요." /> |
|
| 3385 |
+ <FoxHelperText type="success" message="사용할 수 있는 아이디입니다." /> |
|
| 3386 |
+ <FoxHelperText type="warning" message="곧 만료되는 항목입니다." /> |
|
| 3387 |
+ <FoxHelperText type="danger" message="필수 항목입니다." /> |
|
| 3388 |
+ </> |
|
| 3389 |
+ ), |
|
| 3390 |
+ }, |
|
| 3391 |
+ {
|
|
| 3392 |
+ label: "showIcon={false} — 글자만 (폼 헬퍼가 아이콘 없이 쓰이는 자리를 위해)",
|
|
| 3393 |
+ node: ( |
|
| 3394 |
+ <> |
|
| 3395 |
+ <FoxHelperText showIcon={false} message="2자 이상 입력해 주세요." />
|
|
| 3396 |
+ <FoxHelperText type="danger" showIcon={false} message="필수 항목입니다." />
|
|
| 3397 |
+ </> |
|
| 3398 |
+ ), |
|
| 3399 |
+ }, |
|
| 3400 |
+ {
|
|
| 3401 |
+ label: "inline-flex라 폭이 글자만큼입니다 (점선은 자리 확인용)", |
|
| 3402 |
+ node: ( |
|
| 3403 |
+ <div style={{ outline: "1px dashed currentColor" }}>
|
|
| 3404 |
+ <FoxHelperText type="information" message="내용만큼만 차지합니다." /> |
|
| 3405 |
+ </div> |
|
| 3406 |
+ ), |
|
| 3407 |
+ }, |
|
| 3408 |
+ {
|
|
| 3409 |
+ label: "여러 줄 — 아이콘이 첫 줄에 맞춰 고정됩니다", |
|
| 3410 |
+ node: ( |
|
| 3411 |
+ <div style={{ inlineSize: "24rem" }}>
|
|
| 3412 |
+ <FoxHelperText |
|
| 3413 |
+ type="warning" |
|
| 3414 |
+ message="문구가 길어져 두 줄이 되어도 아이콘은 위로 붙어 첫 글줄과 나란히 남습니다." |
|
| 3415 |
+ /> |
|
| 3416 |
+ </div> |
|
| 3417 |
+ ), |
|
| 3418 |
+ }, |
|
| 3419 |
+ ], |
|
| 3420 |
+ }, |
|
| 3372 | 3421 |
]; |
+++ @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/components.scss
+++ @fox/styles/components.scss
... | ... | @@ -20,6 +20,7 @@ |
| 20 | 20 |
@use "fox-email"; |
| 21 | 21 |
@use "fox-file-upload"; |
| 22 | 22 |
@use "fox-form-label"; |
| 23 |
+@use "fox-helper-text"; |
|
| 23 | 24 |
@use "fox-input"; |
| 24 | 25 |
@use "fox-phone-number"; |
| 25 | 26 |
@use "fox-radio"; |
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?