민혜린 민혜린 08-18
feat: 디자인시스템 FoxAlert 추가
@14603a217256ef3583f54bf9e1c8619674a09852
 
@fox/core/components/fox-alert/fox-alert.tsx (added)
+++ @fox/core/components/fox-alert/fox-alert.tsx
@@ -0,0 +1,132 @@
+import type { ReactNode, Ref } from "react";
+import {
+  FoxChatDotsIcon,
+  FoxCheckCircleIcon,
+  FoxInfoIcon,
+  FoxProhibitIcon,
+  FoxWarningIcon,
+} from "../../icons";
+import { cx } from "../../utils";
+
+export type FoxAlertType =
+  | "default"
+  | "information"
+  | "success"
+  | "warning"
+  | "danger";
+
+export interface FoxAlertProps {
+  /**
+   * 알림의 성격. 아이콘·배경·테두리·제목 색이 여기서 갈린다.
+   *
+   * 계열이 바꾸는 것은 배경 · 테두리 · 제목 글자색과 아이콘이다. 본문 글자색은 계열과
+   * 무관하게 같다.
+   *
+   * 아이콘은 계열이 정하고 굵기는 시안대로 `duotone`이며, 색은 제목 글자와 같은 계열색이다.
+   */
+  type?: FoxAlertType;
+  /** 굵은 제목 줄. 넘기지 않으면 영역을 렌더링하지 않는다. */
+  title?: ReactNode;
+  /** 제목 아래 본문. 넘기지 않으면 영역을 렌더링하지 않는다. */
+  message?: ReactNode;
+  /**
+   * 보조기술이 읽는 방식을 정한다(`FoxToast`와 같은 규칙).
+   *
+   * - `false`(기본): `role="status"` — 하던 말을 끊지 않는다. 화면과 함께 처음부터 놓여
+   *   있는 알림은 이쪽이다.
+   * - `true`: `role="alert"` — 읽던 것을 끊고 바로 읽는다. 사용자의 동작 결과로 **그 자리에
+   *   나타난** 알림에만 쓴다.
+   *
+   * `type="danger"`가 자동으로 이걸 켜지 않는다 — 위험한 내용인 것과 지금 끼어들어야 하는
+   * 것은 다른 문제이고, 페이지와 함께 그려지는 경고까지 낭독을 끊으면 방해만 된다.
+   */
+  urgent?: boolean;
+  /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */
+  hidden?: boolean;
+  id?: string;
+  /** 배치 조정용. 시안의 폭(720px)을 바꿔야 할 때도 여기로 준다. */
+  className?: string;
+  ref?: Ref<HTMLDivElement>;
+}
+
+/** `Record`로 고정해 계열을 추가하면 항목 누락이 타입 에러가 되게 한다. */
+const TYPE_CLASS: Record<FoxAlertType, string> = {
+  default: "fox-alert--default",
+  information: "fox-alert--information",
+  success: "fox-alert--success",
+  warning: "fox-alert--warning",
+  danger: "fox-alert--danger",
+};
+
+/**
+ * 계열이 정하는 아이콘. 호출부가 넘기지 않는다 — 무엇을 그릴지는 계열이 이미 답한다
+ * (`FoxTag`와 같은 규칙).
+ *
+ * 시안 `ico/*`가 Phosphor 원본이라 이름이 그대로 붙는다(`ico/Prohibit` →
+ * `FoxProhibitIcon`). 손으로 SVG를 그려 넣지 않는다 — 아이콘은 생성물이 단일 출처다.
+ *
+ * 굵기는 시안이 `Weight=Duotone`이라 다섯 개 모두 `duotone`이다. 기본값(`regular`)으로 두면
+ * 속이 빈 선 그림이 되어 시안과 달라지므로 반드시 적는다.
+ *
+ * duotone은 연한 면(`opacity` 0.2)과 진한 선을 겹쳐 그리는데 **둘 다 `currentColor`다** —
+ * 색은 스타일이 `.fox-alert__icon`에 준 하나를 쓰고, 연한 쪽만 알아서 흐려진다.
+ */
+const TYPE_ICON: Record<FoxAlertType, ReactNode> = {
+  default: <FoxChatDotsIcon weight="duotone" />,
+  information: <FoxInfoIcon weight="duotone" />,
+  success: <FoxCheckCircleIcon weight="duotone" />,
+  warning: <FoxWarningIcon weight="duotone" />,
+  danger: <FoxProhibitIcon weight="duotone" />,
+};
+
+/**
+ * @fox 알럿. 화면 안에 놓이는 띠 모양 알림이다 — 아이콘 상자 · 제목 · 본문이 한 줄로 선다.
+ *
+ * **모달이 아니다.** 화면을 덮지도, 포커스를 가두지도, 확인 버튼을 갖지도 않는다. 내용 흐름
+ * 안에 그대로 놓이고, 언제 나타나고 사라질지는 호출부가 정한다(`FoxToast`와 같은 방식).
+ *
+ * 상호작용이 없어 `"use client"`가 아니다 — 서버 컴포넌트로 렌더된다.
+ *
+ * ⚠️ 시안 주석: **본문은 두 줄 이내**로 쓴다. 길이를 코드가 막지는 않는다 — 세 줄이 되면
+ * 아이콘 상자만 가운데 남고 띠가 늘어난다.
+ *
+ * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"`
+ * (또는 개별 파티셜)로 한 번 불러와야 한다.
+ */
+export function FoxAlert({
+  type = "default",
+  title,
+  message,
+  urgent = false,
+  hidden = false,
+  id,
+  className,
+  ref,
+}: FoxAlertProps) {
+  if (hidden) {
+    return null;
+  }
+
+  return (
+    <div
+      ref={ref}
+      id={id}
+      role={urgent ? "alert" : "status"}
+      className={cx("fox-alert", TYPE_CLASS[type], className)}
+    >
+      {/* 아이콘이 정해지기 전에도 상자는 그린다 — 시안이 흰 판을 배치의 일부로 준다. */}
+      <span className="fox-alert__icon" aria-hidden="true">
+        {TYPE_ICON[type]}
+      </span>
+
+      <div className="fox-alert__content">
+        {title === undefined ? null : (
+          <strong className="fox-alert__title">{title}</strong>
+        )}
+        {message === undefined ? null : (
+          <span className="fox-alert__message">{message}</span>
+        )}
+      </div>
+    </div>
+  );
+}
 
@fox/core/components/fox-alert/index.ts (added)
+++ @fox/core/components/fox-alert/index.ts
@@ -0,0 +1,1 @@
+export { FoxAlert, type FoxAlertProps, type FoxAlertType } from "./fox-alert";
@fox/core/components/index.ts
--- @fox/core/components/index.ts
+++ @fox/core/components/index.ts
@@ -1,4 +1,5 @@
 export * from "./fox-address";
+export * from "./fox-alert";
 export * from "./fox-badge";
 export * from "./fox-badge-dot";
 export * from "./fox-badge-group";
@fox/dev-test/component-registry.tsx
--- @fox/dev-test/component-registry.tsx
+++ @fox/dev-test/component-registry.tsx
@@ -3,6 +3,7 @@
 import { FoxInput, FoxInputState, FoxSelect, FoxSelectItem, FoxSelectOption, FoxSelectOptionItem } from "@/@fox/core/components";
 import { useEffect, useState, type CSSProperties, type ReactNode } from "react";
 import { FoxAddress, type FoxAddressSearchResult, type FoxAddressValue } from "../core/components/fox-address";
+import { FoxAlert } from "../core/components/fox-alert";
 import { FoxBreadcrumb } from "../core/components/fox-breadcrumb";
 import { FoxButton } from "../core/components/fox-button";
 import { FoxButtonGroup } from "../core/components/fox-button-group";
@@ -3319,4 +3320,53 @@
       },
     ],
   },
+  {
+    id: "fox-alert",
+    name: "FoxAlert",
+    description:
+      "Figma 시안(alert 258:433) 폭 720px은 시안 값 그대로지만 좁은 자리에서 부모를 뚫지 않게 max-inline-size: 100%를 함께 걸었습니다. 계열은 배경·테두리·제목 글자색을 바꿉니다 — 테두리만 -inverse 토큰이라 연한 배경(5) 위에 한 단계 진한 선(10)이 얹힙니다. 본문 글자색과 아이콘 상자의 흰 배경은 계열과 무관합니다.",
+    variants: [
+      {
+        label: "default",
+        node: (
+          <FoxAlert
+            title="알림 제목이 들어갑니다"
+            message="콘텐츠 내 알럿은 최대 2줄 이내로 작성합니다  콘텐츠 내 알럿은 최대 2줄 이내로 작성합니다  콘텐츠 내 알럿은 최대 2줄 이내로 작성합니다"
+          />
+        ),
+      },
+      {
+        label: "제목만 / 본문만 — 넘기지 않은 쪽은 렌더하지 않습니다",
+        node: (
+          <>
+            <FoxAlert title="제목만 있는 알림" />
+            <FoxAlert message="본문만 있는 알림입니다." />
+          </>
+        ),
+      },
+      {
+        label: "type 5종",
+        node: (
+          <>
+            <FoxAlert type="default" title="default" message="기본" />
+            <FoxAlert type="information" title="information" message="안내" />
+            <FoxAlert type="success" title="success" message="완료" />
+            <FoxAlert type="warning" title="warning" message="주의" />
+            <FoxAlert type="danger" title="danger" message="위험" />
+          </>
+        ),
+      },
+      {
+        label: "좁은 자리(360px)에 넣으면 폭이 부모에 맞춰 줄어듭니다",
+        node: (
+          <div style={{ inlineSize: "36rem", outline: "1px dashed currentColor" }}>
+            <FoxAlert
+              title="좁은 단 안의 알림"
+              message="720px이 아니라 부모 폭까지만 늘어납니다."
+            />
+          </div>
+        ),
+      },
+    ],
+  },
 ];
 
@fox/styles/_fox-alert.scss (added)
+++ @fox/styles/_fox-alert.scss
@@ -0,0 +1,166 @@
+// FoxAlert — 시안: 통합관리자페이지 디자인시스템 Figma alert
+//
+// 화면 안에 놓이는 띠 모양 알림이다. 아이콘 상자 · 제목 · 본문이 한 줄로 선다.
+// **모달이 아니다** — 화면을 덮지 않고 내용 흐름 안에 그대로 놓인다.
+//
+// 마크업 계약 (React 밖 소비자용):
+//   <div class="fox-alert fox-alert--default" role="status">
+//     <span class="fox-alert__icon">…아이콘…</span>
+//     <div class="fox-alert__content">
+//       <strong class="fox-alert__title">제목</strong>
+//       <span class="fox-alert__message">본문</span>
+//     </div>
+//   </div>
+//
+// 제목·본문은 각각 없을 수 있다. 아이콘 상자는 아이콘이 없어도 그린다 — 시안이 흰 판을
+// 배치의 일부로 주고, 있고 없고에 따라 글자 시작점이 흔들리면 안 된다.
+//
+// 제목이 `heading/xsm`(title 패밀리·bold)이고 본문이 `body/md`(body 패밀리·regular)다.
+// 글자 크기는 둘 다 15px이라 굵기와 패밀리로만 갈린다 — 시안이 그렇게 준다.
+//
+// 토큰이 아닌 값:
+//   - ⚠️ `inline-size: 72rem`: **시안의 폭(720px)을 일단 그대로 적용해 둔 것이다.** Figma가
+//     변수로 export하지 않았고 토큰 스케일에도 720이 없다(modal 계열의 가장 큰 값이 760px).
+//     토큰이 생기면 `fox.*()`로 바꾸고, 폭을 고정하지 않는 쪽으로 정해지면 여기서 뺀다.
+//     그때까지 폭을 바꿔야 하는 자리는 호출부가 `className`으로 덮는다
+//     (`fox-toggle-quantity`와 같은 처리).
+//   - `max-inline-size: 100%`: 시안에 없는 구조 규칙이다. 720px보다 좁은 자리에 놓였을 때
+//     띠가 부모를 뚫고 나가지 않게 막는다 — 고정 폭만 두면 표 안이나 좁은 단에서 넘친다.
+//   - line-height / letter-spacing: 시안 텍스트 스타일 heading/xsm·body/md의 값이나 Figma가
+//     변수로 export하지 않는다. 시안의 -0.375px은 15px 기준이라 `-0.025em`으로 적는다
+//     (다른 컴포넌트와 같은 근거).
+//   - box-sizing / display / flex / aspect-ratio: 구조 규칙.
+//
+// 아이콘은 컴포넌트의 `TYPE_ICON`이 계열마다 하나씩 쥐고 있다(Prohibit · Warning ·
+// CheckCircle · Info · ChatDots, 전부 `weight="duotone"`). 색은 **제목 글자와 같은 계열색**을
+// 쓴다(default는 `font-neutral-strong`) — 시안 확인 완료.
+//
+// duotone은 연한 면과 진한 선을 겹쳐 그리고 **둘 다 `currentColor`를 쓴다** — 연한 쪽은
+// 글리프 자체가 `opacity: 0.2`를 물고 있어 색을 하나만 줘도 두 톤이 나온다.
+//
+// duotone은 연한 면과 진한 선을 겹쳐 그리고 **둘 다 `currentColor`를 쓴다** — 연한 쪽은
+// 글리프 자체가 `opacity: 0.2`를 물고 있어 색을 하나만 줘도 두 톤이 나온다.
+//
+
+@use "@fox/styles/abstracts" as fox;
+
+.fox-alert {
+  // 안쪽 여백이 바깥 폭을 늘리지 않게 한다 — 720px이 720px로 그려진다.
+  box-sizing: border-box;
+  display: flex;
+  align-items: center;
+  gap: fox.gap(4);
+  inline-size: 72rem;
+  max-inline-size: 100%;
+  padding: fox.padding(5) fox.padding(6);
+  border: fox.border(1) solid fox.color(border-neutral-subtler);
+  border-radius: fox.radius(4);
+  background: fox.color(surface-neutral-gray);
+
+  // 정사각 흰 판. 글자가 길어져 띠가 늘어나도 크기가 그대로여야 해서 줄어들지 않게 막는다.
+  &__icon {
+    display: flex;
+    flex-shrink: 0;
+    justify-content: center;
+    align-items: center;
+    inline-size: fox.number(13);
+    block-size: fox.number(13);
+    aspect-ratio: 1 / 1;
+    border-radius: fox.radius(4);
+    background: fox.color(element-neutral-inverse);
+    // 글리프가 `fill: currentColor`라 이 색을 그대로 입는다. 계열이 있으면 아래에서 덮는다.
+    color: fox.color(font-neutral-strong);
+  }
+
+  // 글리프는 흰 판(36px)보다 작은 24px이다 — 판이 글리프를 감싸는 여백을 갖는다.
+  // 상자와 마찬가지로 줄어들지 않게 막고 정사각을 못박는다(시안이 둘 다 명시한다).
+  &__icon > * {
+    flex-shrink: 0;
+    inline-size: fox.icon(5);
+    block-size: fox.icon(5);
+    aspect-ratio: 1 / 1;
+  }
+
+  // 남는 폭을 전부 가져간다. `min-inline-size: 0`이 없으면 긴 글이 줄바꿈되지 않고 띠를
+  // 밀어 넓힌다(flex 자식의 기본 최소 크기가 내용 크기라서다).
+  &__content {
+    display: flex;
+    flex: 1 0 0;
+    flex-direction: column;
+    align-items: flex-start;
+    gap: fox.gap(1);
+    min-inline-size: 0;
+  }
+
+  &__title {
+    align-self: stretch;
+    color: fox.color(font-neutral-strong);
+    font-family: fox.font-family(title);
+    font-size: fox.font-size(heading-xsm);
+    font-weight: fox.font-weight(bold);
+    line-height: 1.4;
+    letter-spacing: -0.025em;
+  }
+
+  &__message {
+    align-self: stretch;
+    color: fox.color(font-neutral-strong);
+    font-family: fox.font-family(body);
+    font-size: fox.font-size(body-md);
+    font-weight: fox.font-weight(regular);
+    line-height: 1.5;
+    letter-spacing: -0.025em;
+  }
+
+  // ── 계열 ────────────────────────────────────────────────────────────────────
+  // 계열이 바꾸는 것은 **배경 · 테두리 · 제목 글자색** 셋뿐이다. 본문 글자색과 아이콘 상자의
+  // 흰 배경은 계열과 무관하게 블록 규칙을 그대로 쓴다(시안 확인).
+  //
+  // 테두리만 `-inverse`다 — 같은 계열의 5(연한 배경)와 10(그보다 한 단계 진한 선)을 짝지어
+  // 쓰라고 토큰이 그렇게 갈라져 있다. 다크 모드에서는 세 값이 함께 뒤집힌다(95 / 90 / 20).
+  //
+  // `default`는 블록 규칙이 곧 그 계열의 모양이라 따로 덮을 것이 없다.
+  &--default {
+    // 시안 그대로 — 블록에 있는 값이 전부다.
+  }
+
+  &--information {
+    border-color: fox.color(border-system-information-inverse);
+    background: fox.color(surface-system-information);
+  }
+
+  &--information &__title,
+  &--information &__icon {
+    color: fox.color(font-system-information);
+  }
+
+  &--success {
+    border-color: fox.color(border-system-success-inverse);
+    background: fox.color(surface-system-success);
+  }
+
+  &--success &__title,
+  &--success &__icon {
+    color: fox.color(font-system-success);
+  }
+
+  &--warning {
+    border-color: fox.color(border-system-warning-inverse);
+    background: fox.color(surface-system-warning);
+  }
+
+  &--warning &__title,
+  &--warning &__icon {
+    color: fox.color(font-system-warning);
+  }
+
+  &--danger {
+    border-color: fox.color(border-system-danger-inverse);
+    background: fox.color(surface-system-danger);
+  }
+
+  &--danger &__title,
+  &--danger &__icon {
+    color: fox.color(font-system-danger);
+  }
+}
@fox/styles/components.scss
--- @fox/styles/components.scss
+++ @fox/styles/components.scss
@@ -8,6 +8,7 @@
 @use "fox-button-group";
 @use "fox-button-panel";
 @use "fox-address";
+@use "fox-alert";
 @use "fox-certified";
 @use "fox-check-icon";
 @use "fox-checkbox";
@@ -51,4 +52,4 @@
 @use "fox-tab-item";
 @use "fox-spinner";
 @use "fox-toast";
-@use "fox-toggle-quantity";
+@use "fox-toggle-quantity";
(파일 끝에 줄바꿈 문자 없음)
Add a comment
List