민혜린 민혜린 08-19
feat: 디자인시스템 FoxSnackbar, FoxSanackbarList 추가
@ea426c39334890fbf873105d7fab5b1d86d3134d
 
@fox/core/components/fox-snackbar-list/fox-snackbar-list.tsx (added)
+++ @fox/core/components/fox-snackbar-list/fox-snackbar-list.tsx
@@ -0,0 +1,56 @@
+import type { ReactNode, Ref } from "react";
+import { cx } from "../../utils";
+
+export interface FoxSnackbarListProps {
+  /** 묶음을 읽어 줄 이름. 화면에 보이는 제목이 이미 있으면 `labelledBy`를 쓴다. */
+  label?: string;
+  /** 묶음 이름 역할을 하는 요소의 id. `label`보다 우선한다 — 화면의 글자와 어긋나지 않는다. */
+  labelledBy?: string;
+  /** `FoxSnackbar`들. */
+  children?: ReactNode;
+  /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */
+  hidden?: boolean;
+  /** 배치 조정용. 띄우는 자리(fixed·bottom 등)와 시안의 폭(640px)을 여기로 덮는다. */
+  className?: string;
+  ref?: Ref<HTMLDivElement>;
+}
+
+/**
+ * @fox 스낵바 묶음. 스낵바가 여러 개일 때 세로로 쌓는다. 배치만 책임지고 상태를 갖지 않는다 —
+ * 무엇이 몇 개 떠 있는지, 어떤 순서로 쌓을지는 호출부가 소유한다.
+ *
+ * 자식에게 덮어쓰는 값이 없다(`FoxTagList`·`FoxChipArea`와 다른 점이다) — 스낵바에는 계열도
+ * 크기도 없어서 묶음이 통일할 것이 폭과 간격뿐이다.
+ *
+ * 방향 수식어가 없다 — 스낵바는 세로로 쌓이는 것이 유일한 배치다.
+ *
+ * `role="group"`이다. 각 스낵바가 스스로 `role="status"`로 읽히므로 묶음은 자리만 만든다 —
+ * 묶음까지 live region으로 만들면 하나가 뜰 때마다 목록 전체가 다시 읽힌다.
+ *
+ * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"`
+ * (또는 개별 파티셜)로 한 번 불러와야 한다.
+ */
+export function FoxSnackbarList({
+  label,
+  labelledBy,
+  children,
+  hidden = false,
+  className,
+  ref,
+}: FoxSnackbarListProps) {
+  if (hidden) {
+    return null;
+  }
+
+  return (
+    <div
+      ref={ref}
+      role="group"
+      aria-label={labelledBy ? undefined : label}
+      aria-labelledby={labelledBy}
+      className={cx("fox-snackbar-list", className)}
+    >
+      {children}
+    </div>
+  );
+}
 
@fox/core/components/fox-snackbar-list/index.ts (added)
+++ @fox/core/components/fox-snackbar-list/index.ts
@@ -0,0 +1,1 @@
+export { FoxSnackbarList, type FoxSnackbarListProps } from "./fox-snackbar-list";
 
@fox/core/components/fox-snackbar/fox-snackbar.tsx (added)
+++ @fox/core/components/fox-snackbar/fox-snackbar.tsx
@@ -0,0 +1,131 @@
+"use client";
+
+import type { ReactNode, Ref } from "react";
+import { FoxXIcon } from "../../icons";
+import { cx } from "../../utils";
+import { FoxTextButton } from "../fox-text-button";
+
+export interface FoxSnackbarProps {
+  /**
+   * 글자 앞에 붙는 아이콘. 넘기지 않으면 자리를 만들지 않는다.
+   *
+   * 계열이 없는 조각이라 무엇을 그릴지는 호출부가 정한다(`FoxAlert`처럼 계열이 아이콘을
+   * 고르지 않는다). `@fox/core/icons`에서 가져다 넘긴다 — 시안은 `fill` 굵기다:
+   * `<FoxCheckCircleIcon weight="fill" />`.
+   *
+   * 색은 스타일이 정한다(`icon-theme-primary`) — 글리프가 `fill: currentColor`라 넘기는
+   * 쪽에서 색을 줄 필요가 없다.
+   */
+  icon?: ReactNode;
+  /** 굵은 첫 줄. 넘기지 않으면 영역을 렌더링하지 않는다 — 본문만 있는 스낵바가 된다. */
+  title?: ReactNode;
+  /** 제목 아래 본문. 넘기지 않으면 영역을 렌더링하지 않는다. */
+  message?: ReactNode;
+  /**
+   * 오른쪽 글자 버튼의 문구. 없으면 버튼을 그리지 않는다.
+   *
+   * 버튼은 `FoxTextButton`의 `primary`·`md`로 고정이다(시안 지정) — 계열·크기를 열지 않는
+   * 것은 스낵바마다 버튼 모양이 달라지면 같은 조각으로 읽히지 않기 때문이다.
+   */
+  actionLabel?: string;
+  /** 글자 버튼을 눌렀을 때. `actionLabel`이 없으면 의미 없다. */
+  onAction?: () => void;
+  /**
+   * 닫기(X)를 눌렀을 때. **필수다** — 시안이 닫기를 항상 그리므로, 눌러도 아무 일이 없는
+   * 버튼이 남지 않게 호출부가 반드시 처리를 준다.
+   */
+  onClose: () => void;
+  /** 닫기 버튼을 읽어 줄 이름. X 아이콘은 장식이라 이 글자가 유일한 이름이다. */
+  closeLabel?: string;
+  /**
+   * 보조기술이 읽는 방식을 정한다(`FoxToast`·`FoxAlert`와 같은 규칙).
+   *
+   * - `false`(기본): `role="status"` — 하던 말을 끊지 않는다.
+   * - `true`: `role="alert"` — 읽던 것을 끊고 바로 읽는다. 지금 손을 멈춰야 하는 알림에만.
+   */
+  urgent?: boolean;
+  /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */
+  hidden?: boolean;
+  id?: string;
+  /** 배치 조정용. 띄우는 자리(fixed·bottom 등)와 시안의 폭(640px)을 여기로 덮는다. */
+  className?: string;
+  ref?: Ref<HTMLDivElement>;
+}
+
+/**
+ * @fox 스낵바. 흰 판 위에 굵은 한 줄과 글자 버튼이 서고, 오른쪽 끝에 닫기가 붙는다.
+ * 크기 수식어가 없다 — 시안에 하나뿐이다.
+ *
+ * `FoxToast`와 형제지만 **누를 것이 있다.** 토스트는 읽고 지나가는 조각이라 스스로 사라지고,
+ * 스낵바는 사용자가 되돌리거나(실행 취소) 이어서 할 일이 있어 손이 닿을 때까지 남는다.
+ *
+ * **상태를 갖지 않는다.** 언제 뜨고 언제 사라질지, 화면 어디에 놓일지는 전부 호출부가 정한다
+ * — `onClose`는 "닫아 달라"는 알림이지 이 조각이 스스로 사라지는 것이 아니다.
+ *
+ * ⚠️ 보조기술은 **이미 화면에 있던 live region의 내용이 바뀔 때** 읽는다. 이 요소를 통째로
+ * 새로 붙이면 조합에 따라 읽히지 않을 수 있다(`FoxToast`와 같은 사정).
+ *
+ * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"`
+ * (또는 개별 파티셜)로 한 번 불러와야 한다.
+ */
+export function FoxSnackbar({
+  icon,
+  title,
+  message,
+  actionLabel,
+  onAction,
+  onClose,
+  closeLabel = "닫기",
+  urgent = false,
+  hidden = false,
+  id,
+  className,
+  ref,
+}: FoxSnackbarProps) {
+  if (hidden) {
+    return null;
+  }
+
+  return (
+    <div
+      ref={ref}
+      id={id}
+      role={urgent ? "alert" : "status"}
+      className={cx("fox-snackbar", className)}
+    >
+      {icon ? (
+        <span className="fox-snackbar__icon" aria-hidden="true">
+          {icon}
+        </span>
+      ) : null}
+
+      <div className="fox-snackbar__content">
+        {title === undefined ? null : (
+          <strong className="fox-snackbar__title">{title}</strong>
+        )}
+        {message === undefined ? null : (
+          <span className="fox-snackbar__message">{message}</span>
+        )}
+      </div>
+
+      {actionLabel ? (
+        <FoxTextButton
+          type="primary"
+          size="md"
+          label={actionLabel}
+          onAction={onAction}
+          className="fox-snackbar__action"
+        />
+      ) : null}
+
+      <button
+        type="button"
+        className="fox-snackbar__close"
+        aria-label={closeLabel}
+        onClick={onClose}
+      >
+        <FoxXIcon />
+      </button>
+    </div>
+  );
+}
 
@fox/core/components/fox-snackbar/index.ts (added)
+++ @fox/core/components/fox-snackbar/index.ts
@@ -0,0 +1,1 @@
+export { FoxSnackbar, type FoxSnackbarProps } from "./fox-snackbar";
@fox/core/components/index.ts
--- @fox/core/components/index.ts
+++ @fox/core/components/index.ts
@@ -38,6 +38,8 @@
 export * from "./fox-select-option";
 export * from "./fox-select-option-item";
 export * from "./fox-select-text";
+export * from "./fox-snackbar";
+export * from "./fox-snackbar-list";
 export * from "./fox-spinner";
 export * from "./fox-status-indicator";
 export * from "./fox-tab";
@fox/dev-test/component-registry.tsx
--- @fox/dev-test/component-registry.tsx
+++ @fox/dev-test/component-registry.tsx
@@ -53,6 +53,9 @@
 import { FoxTab } from "../core/components/fox-tab";
 import { FoxTabItem } from "../core/components/fox-tab-item";
 import { FoxSpinner } from "../core/components/fox-spinner";
+import { FoxCheckCircleIcon } from "../core/icons";
+import { FoxSnackbar } from "../core/components/fox-snackbar";
+import { FoxSnackbarList } from "../core/components/fox-snackbar-list";
 import { FoxStatusIndicator } from "../core/components/fox-status-indicator";
 import { FoxChip, type FoxChipItem } from "../core/components/fox-chip";
 import { FoxChipArea } from "../core/components/fox-chip-area";
@@ -3447,4 +3450,97 @@
       },
     ],
   },
+  {
+    id: "fox-snackbar",
+    name: "FoxSnackbar",
+    description:
+      "Figma 시안(snackbar 441:5880) 흰 판 위에 굵은 한 줄과 글자 버튼이 서고 오른쪽 끝에 닫기가 붙습니다. FoxToast와 형제지만 누를 것이 있어서, 읽고 지나가는 토스트와 달리 손이 닿을 때까지 남습니다. 상태를 갖지 않습니다 — onClose는 '닫아 달라'는 알림이지 스스로 사라지는 것이 아니고, 위치도 호출부가 정합니다. 글자 버튼은 FoxTextButton의 primary·md, 닫기는 오른쪽에 붙습니다 — 판의 오른쪽 여백 64px은 글자가 그 아래로 들어가지 않게 비워 두는 자리입니다.",
+    variants: [
+      {
+        label: "기본 — 제목 + 본문 + 액션 + 닫기",
+        node: (
+          <FoxSnackbar
+            title="메세지 타이틀"
+            message="스낵바는 화면 하단에 제공되는 안내 메시지입니다."
+            actionLabel="텍스트 버튼"
+            onAction={() => {}}
+            onClose={() => {}}
+          />
+        ),
+      },
+      {
+        label: "아이콘 — 호출부가 @fox/core/icons에서 골라 넘깁니다 (ico/CheckCircle, weight=fill / 색은 icon-theme-primary)",
+        node: (
+          <FoxSnackbar
+            icon={<FoxCheckCircleIcon weight="fill" />}
+            title="변경 사항을 저장했습니다"
+            message="3분 안에는 되돌릴 수 있습니다."
+            actionLabel="실행 취소"
+            onAction={() => {}}
+            onClose={() => {}}
+          />
+        ),
+      },
+      {
+        label: "제목 없이 / 액션 없이 — 넘기지 않은 쪽은 그리지 않습니다 (닫기는 항상 있습니다)",
+        node: (
+          <>
+            <FoxSnackbar title="제목만 있는 스낵바" actionLabel="보기" onAction={() => {}} onClose={() => {}} />
+            <FoxSnackbar message="제목 없이 본문만 있는 스낵바입니다." actionLabel="보기" onAction={() => {}} onClose={() => {}} />
+            <FoxSnackbar title="액션이 없는 스낵바" message="닫기만 있습니다." onClose={() => {}} />
+          </>
+        ),
+      },
+      {
+        label: "제목이 길면 줄바꿈되고 버튼은 오른쪽에 남습니다",
+        node: (
+          <FoxSnackbar
+            title="제목이 길어져 두 줄이 되어도"
+            message="글자 버튼과 닫기는 자리를 지킵니다. 판은 640px에서 시작하고 오른쪽 64px은 닫기 자리로 비워 둡니다."
+            actionLabel="자세히"
+            onAction={() => {}}
+            onClose={() => {}}
+          />
+        ),
+      },
+    ],
+  },
+  {
+    id: "fox-snackbar-list",
+    name: "FoxSnackbarList",
+    description:
+      "Figma 시안(snackbar-list 467:5958) 스낵바가 여러 개일 때 세로로 쌓는 묶음입니다. — 무엇이 몇 개 떠 있는지, 어떤 순서로 쌓을지는 호출부가 소유합니다. 자식에게 덮어쓰는 값이 없다는 점이 FoxTagList·FoxChipArea와 다릅니다.",
+    variants: [
+      {
+        label: "세 개가 쌓인 모습 (간격 8px)",
+        node: (
+          <FoxSnackbarList label="알림">
+            <FoxSnackbar
+              icon={<FoxCheckCircleIcon weight="fill" />}
+              title="변경 사항을 저장했습니다"
+              message="3분 안에는 되돌릴 수 있습니다."
+              actionLabel="실행 취소"
+              onAction={() => {}}
+              onClose={() => {}}
+            />
+            <FoxSnackbar
+              title="파일 3개를 올렸습니다"
+              actionLabel="보기"
+              onAction={() => {}}
+              onClose={() => {}}
+            />
+            <FoxSnackbar message="제목 없이 본문만 있는 스낵바입니다." onClose={() => {}} />
+          </FoxSnackbarList>
+        ),
+      },
+      {
+        label: "하나만 있어도 그대로 씁니다",
+        node: (
+          <FoxSnackbarList label="알림">
+            <FoxSnackbar title="하나만 떠 있는 경우" onClose={() => {}} />
+          </FoxSnackbarList>
+        ),
+      },
+    ],
+  },
 ];
 
@fox/styles/_fox-snackbar-list.scss (added)
+++ @fox/styles/_fox-snackbar-list.scss
@@ -0,0 +1,38 @@
+// FoxSnackbarList — 시안: 통합관리자페이지 디자인시스템 Figma snackbar-list
+//
+// 스낵바가 여러 개일 때 세로로 쌓는 묶음이다. 배치만 갖는다 — 무엇이 몇 개 떠 있는지는
+// 호출부가 소유한다.
+//
+// 마크업 계약 (React 밖 소비자용):
+//   <div class="fox-snackbar-list" role="group">
+//     …fox-snackbar…
+//     …fox-snackbar…
+//   </div>
+//
+// 자식 폭과 묶음 폭이 같은 640px이다 — 묶음이 자식을 늘이거나 줄이지 않고, `align-items`가
+// `flex-start`라 자식이 자기 폭을 그대로 갖는다(시안 지정).
+//
+// 토큰이 아닌 값:
+//   - ⚠️ `inline-size: 64rem`: **시안의 폭(640px)을 그대로 적용해 둔 것이다.** 자식
+//     `fox-snackbar`와 같은 값이고 근거도 같다 — 토큰 스케일에 640이 없다. 폭을 바꿔야 하는
+//     자리는 호출부가 `className`으로 덮는다.
+//   - display / flex-direction / align-items: 구조 규칙.
+//
+// ⚠️ 모바일에서는 자식과 같은 이유로 고정 폭을 푼다 — 640px을 그대로 두면 화면 밖으로 넘쳐
+// 잘린다(`fox-snackbar` 상단 주석 참고).
+
+@use "@fox/styles/abstracts" as fox;
+
+.fox-snackbar-list {
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  gap: fox.gap(3);
+  inline-size: 64rem;
+
+  // 모바일에서는 부모 폭을 채운다. 자식(`fox-snackbar`)도 같은 규칙을 스스로 갖는다.
+  @include fox.mobile {
+    inline-size: 100%;
+  }
+}
 
@fox/styles/_fox-snackbar.scss (added)
+++ @fox/styles/_fox-snackbar.scss
@@ -0,0 +1,181 @@
+// FoxSnackbar — 시안: 통합관리자페이지 디자인시스템 Figma snackbar
+//
+// 흰 판 위에 글자(제목·본문)와 글자 버튼이 서고, 오른쪽 위에 닫기가 붙는다. 제목과 본문은
+// 각각 없을 수 있다 — 한쪽만 있으면 그 줄만 그린다. `FoxToast`와 형제지만
+// **누를 것이 있다** — 그래서 스스로 사라지지 않고 손이 닿을 때까지 남는다.
+//
+// 마크업 계약 (React 밖 소비자용):
+//   <div class="fox-snackbar" role="status">
+//     <span class="fox-snackbar__icon">…아이콘 svg…</span>              <!-- 선택 -->
+//     <div class="fox-snackbar__content">
+//       <strong class="fox-snackbar__title">저장했습니다</strong>            <!-- 선택 -->
+//       <span class="fox-snackbar__message">3분 안에 되돌릴 수 있습니다.</span>  <!-- 선택 -->
+//     </div>
+//     <button class="fox-text-button fox-text-button--primary fox-text-button--md
+//                    fox-snackbar__action">실행 취소</button>          <!-- 선택 -->
+//     <button class="fox-snackbar__close" aria-label="닫기">…X svg…</button>  <!-- 선택 -->
+//   </div>
+//
+// 글자 버튼은 `FoxTextButton`의 primary·md를 그대로 쓴다 — 스낵바가 버튼 모양을 다시 정하지
+// 않고 `fox-snackbar__action`은 배치만 맡는다.
+//
+// 토큰이 아닌 값:
+//   - ⚠️ `inline-size` / `min-inline-size: 64rem`: **시안의 폭(640px)을 그대로 적용해 둔
+//     것이다.** Figma가 변수로 export하지 않았고 토큰 스케일에도 640이 없다. 폭을 바꿔야 하는
+//     자리는 호출부가 `className`으로 덮는다(`fox-toggle-quantity`·`fox-alert`와 같은 처리).
+//   - ⚠️ `padding-inline-end`가 64px이라 토큰이 없다. 닫기가 `right: 0`으로 오른쪽 끝에
+//     붙으므로(시안 지정) 이 여백은 **글자·버튼이 닫기 밑으로 들어가지 않게 비워 두는
+//     자리**다. 20px + 아이콘 24px + 20px = 64px로 떨어져서 그 조합으로 적는다
+//     (`padding(7)` + `icon(5)` + `padding(7)`).
+//   - line-height / letter-spacing: 시안 텍스트 스타일 heading/xsm의 값이나 Figma가 변수로
+//     export하지 않는다. -0.375px은 15px 기준이라 `-0.025em`으로 적는다(다른 컴포넌트와 같은
+//     근거).
+//   - box-sizing / position / display / flex: 구조 규칙.
+//
+// ⚠️ 그림자 색: 시안이 두 겹을 각각 `dark/alpha/black10`·`alpha/black10`으로 물려 놓았는데
+// 값이 같고(rgba(0,0,0,0.10)) 앞쪽 이름의 토큰은 export에 없다. 두 겹 모두 `alpha-black10`을
+// 쓴다 — `fox-toast`·`fox-select-option`과 같은 선택이다.
+//
+// ⚠️ 폭 세 값(640 / 640 / `grid(wrap-sm)`)은 **PC 기준이다.** 모바일에서 `grid(wrap-sm)`은
+// 320px인데 최소 폭 640px이 그것을 이겨 화면 밖으로 넘친다 — 실제로 잘렸다. 그래서 모바일에서는
+// 최소 폭을 풀고 판이 부모 폭을 채우게 둔다(아래 `fox.mobile` 블록).
+//
+// 시안이 모바일 스낵바의 폭을 따로 주면 그 값으로 바꾼다. 지금은 "화면을 넘지 않는다"만
+// 보장한다.
+
+@use "@fox/styles/abstracts" as fox;
+
+.fox-snackbar {
+  // 안쪽 여백이 바깥 폭을 늘리지 않게 한다 — 640px이 640px로 그려진다.
+  box-sizing: border-box;
+  // 닫기 버튼의 위치 기준이 된다.
+  position: relative;
+  display: flex;
+  align-items: center;
+  gap: fox.gap(5);
+  inline-size: 64rem;
+  min-inline-size: 64rem;
+  max-inline-size: fox.grid(wrap-sm);
+  padding-block: fox.padding(6);
+  padding-inline-start: fox.padding(7);
+  // 닫기 버튼이 앉을 자리(위 주석 참고).
+  padding-inline-end: calc(#{fox.padding(7)} + #{fox.icon(5)} + #{fox.padding(7)});
+  border: fox.border(1) solid fox.color(border-neutral-subtle);
+  border-radius: fox.radius(5);
+  background: fox.color(surface-neutral-default);
+
+  // 시안의 neutral/lv2 — 가까운 그림자와 먼 그림자 두 겹이다.
+  box-shadow:
+    fox.shadow(location1) fox.shadow(location1) fox.shadow(blur2) 0
+      fox.primitive(alpha-black10),
+    fox.shadow(location3) fox.shadow(location3) fox.shadow(blur3) 0
+      fox.primitive(alpha-black10);
+
+  // 모바일에서는 640px 고정을 풀고 부모 폭을 채운다(위 주석 참고). `min-inline-size`를 함께
+  // 풀지 않으면 640px이 그대로 남아 화면 밖으로 밀려 잘린다.
+  @include fox.mobile {
+    inline-size: 100%;
+    min-inline-size: 0;
+    max-inline-size: 100%;
+  }
+
+  // 글자 앞 아이콘. 글자가 길어 판이 높아져도 크기가 그대로여야 해서 줄어들지 않게 막는다.
+  //
+  // 색은 `icon-theme-primary`다(시안 확인). 글리프가 `fill: currentColor`라 슬롯에 색만
+  // 주면 그대로 입는다 — 굵기는 호출부가 넘기는 글리프가 정하고 시안은 `fill`이다.
+  //
+  // 크기는 시안 값 그대로 24px(`icon(5)`)이고 닫기 아이콘과 같다. 세로 정렬은 시안에 없어
+  // 판 규칙(가운데)을 그대로 따른다.
+  &__icon {
+    display: inline-flex;
+    flex-shrink: 0;
+    align-items: center;
+    justify-content: center;
+    inline-size: fox.icon(5);
+    block-size: fox.icon(5);
+    aspect-ratio: 1 / 1;
+    color: fox.color(icon-theme-primary);
+  }
+
+  // 넘긴 SVG가 슬롯을 꽉 채운다 — 크기는 슬롯이 정하고 SVG는 따라온다.
+  &__icon > * {
+    inline-size: 100%;
+    block-size: 100%;
+  }
+
+  // 남는 폭을 전부 가져가 글자 버튼을 오른쪽 끝으로 민다. `min-inline-size: 0`이 없으면
+  // 긴 글이 줄바꿈되지 않고 판을 밀어 넓힌다(flex 자식의 기본 최소 크기가 내용 크기라서다).
+  //
+  // `flex: 1 0 0`으로 남는 폭을 전부 가져가 글자 버튼을 오른쪽 끝으로 민다. 제목과 본문
+  // 사이는 `gap(1)`(2px)이고 둘 다 왼쪽에 붙는다 — 시안 확인 완료(`FoxAlert`의 같은 자리와
+  // 같은 값이다).
+  &__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;
+  }
+
+  // 글자 버튼은 자기 모양을 그대로 갖는다 — 여기서는 줄어들지만 않게 막는다.
+  &__action {
+    flex-shrink: 0;
+  }
+
+  // 오른쪽 끝에 붙는다(시안 지정 `right: 0`). 흐름에서 빠져 있어 글자·버튼 배치에 끼어들지
+  // 않고, 판의 오른쪽 여백(64px)이 그 아래로 글자가 들어가지 않게 막아 준다.
+  //
+  // 누르는 자리는 48px 정사각이고 아이콘은 24px이다 — 손가락이 닿는 넓이를 아이콘보다 크게
+  // 잡은 것이라, 여백을 줄이면 모바일에서 누르기 어려워진다.
+  //
+  // ⚠️ 시안이 이 칸에 `align-items: flex-end`를 준다. 가로로는 시작(왼쪽), 세로로는 끝(아래)에
+  // 붙어서 **아이콘이 48px 칸의 왼쪽 아래로 치우친다** — 오른쪽 끝에서 16px, 아래에서 8px이다.
+  // 시안 값을 그대로 옮긴 결과이니, 화면에서 가운데로 보여야 하면 `center`로 바꾼다.
+  //
+  // 세로는 판 위쪽에 붙는다(시안 지정 `top: 0`). 가운데 정렬이 아니라서, 본문이 늘어 판이
+  // 높아져도 닫기는 첫 줄 옆에 그대로 남는다.
+  &__close {
+    position: absolute;
+    inset-inline-end: 0;
+    inset-block-start: 0;
+    display: flex;
+    align-items: flex-end;
+    inline-size: fox.number(16);
+    block-size: fox.number(16);
+    aspect-ratio: 1 / 1;
+    padding: fox.padding(3);
+    border: none;
+    background: none;
+    color: fox.color(icon-neutral-default);
+    cursor: pointer;
+  }
+
+  // 아이콘은 24px 정사각으로 못박는다 — 칸이 48px이라 늘어나게 두면 안 된다.
+  &__close > * {
+    flex-shrink: 0;
+    inline-size: fox.icon(5);
+    block-size: fox.icon(5);
+    aspect-ratio: 1 / 1;
+  }
+}
@fox/styles/components.scss
--- @fox/styles/components.scss
+++ @fox/styles/components.scss
@@ -51,6 +51,8 @@
 @use "fox-table-cell";
 @use "fox-tab";
 @use "fox-tab-item";
+@use "fox-snackbar";
+@use "fox-snackbar-list";
 @use "fox-spinner";
 @use "fox-status-indicator";
 @use "fox-toast";
Add a comment
List