+++ @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/index.ts
+++ @fox/core/components/index.ts
... | ... | @@ -38,6 +38,8 @@ |
| 38 | 38 |
export * from "./fox-select-option"; |
| 39 | 39 |
export * from "./fox-select-option-item"; |
| 40 | 40 |
export * from "./fox-select-text"; |
| 41 |
+export * from "./fox-snackbar"; |
|
| 42 |
+export * from "./fox-snackbar-list"; |
|
| 41 | 43 |
export * from "./fox-spinner"; |
| 42 | 44 |
export * from "./fox-status-indicator"; |
| 43 | 45 |
export * from "./fox-tab"; |
--- @fox/dev-test/component-registry.tsx
+++ @fox/dev-test/component-registry.tsx
... | ... | @@ -53,6 +53,9 @@ |
| 53 | 53 |
import { FoxTab } from "../core/components/fox-tab";
|
| 54 | 54 |
import { FoxTabItem } from "../core/components/fox-tab-item";
|
| 55 | 55 |
import { FoxSpinner } from "../core/components/fox-spinner";
|
| 56 |
+import { FoxCheckCircleIcon } from "../core/icons";
|
|
| 57 |
+import { FoxSnackbar } from "../core/components/fox-snackbar";
|
|
| 58 |
+import { FoxSnackbarList } from "../core/components/fox-snackbar-list";
|
|
| 56 | 59 |
import { FoxStatusIndicator } from "../core/components/fox-status-indicator";
|
| 57 | 60 |
import { FoxChip, type FoxChipItem } from "../core/components/fox-chip";
|
| 58 | 61 |
import { FoxChipArea } from "../core/components/fox-chip-area";
|
... | ... | @@ -3447,4 +3450,97 @@ |
| 3447 | 3450 |
}, |
| 3448 | 3451 |
], |
| 3449 | 3452 |
}, |
| 3453 |
+ {
|
|
| 3454 |
+ id: "fox-snackbar", |
|
| 3455 |
+ name: "FoxSnackbar", |
|
| 3456 |
+ description: |
|
| 3457 |
+ "Figma 시안(snackbar 441:5880) 흰 판 위에 굵은 한 줄과 글자 버튼이 서고 오른쪽 끝에 닫기가 붙습니다. FoxToast와 형제지만 누를 것이 있어서, 읽고 지나가는 토스트와 달리 손이 닿을 때까지 남습니다. 상태를 갖지 않습니다 — onClose는 '닫아 달라'는 알림이지 스스로 사라지는 것이 아니고, 위치도 호출부가 정합니다. 글자 버튼은 FoxTextButton의 primary·md, 닫기는 오른쪽에 붙습니다 — 판의 오른쪽 여백 64px은 글자가 그 아래로 들어가지 않게 비워 두는 자리입니다.", |
|
| 3458 |
+ variants: [ |
|
| 3459 |
+ {
|
|
| 3460 |
+ label: "기본 — 제목 + 본문 + 액션 + 닫기", |
|
| 3461 |
+ node: ( |
|
| 3462 |
+ <FoxSnackbar |
|
| 3463 |
+ title="메세지 타이틀" |
|
| 3464 |
+ message="스낵바는 화면 하단에 제공되는 안내 메시지입니다." |
|
| 3465 |
+ actionLabel="텍스트 버튼" |
|
| 3466 |
+ onAction={() => {}}
|
|
| 3467 |
+ onClose={() => {}}
|
|
| 3468 |
+ /> |
|
| 3469 |
+ ), |
|
| 3470 |
+ }, |
|
| 3471 |
+ {
|
|
| 3472 |
+ label: "아이콘 — 호출부가 @fox/core/icons에서 골라 넘깁니다 (ico/CheckCircle, weight=fill / 색은 icon-theme-primary)", |
|
| 3473 |
+ node: ( |
|
| 3474 |
+ <FoxSnackbar |
|
| 3475 |
+ icon={<FoxCheckCircleIcon weight="fill" />}
|
|
| 3476 |
+ title="변경 사항을 저장했습니다" |
|
| 3477 |
+ message="3분 안에는 되돌릴 수 있습니다." |
|
| 3478 |
+ actionLabel="실행 취소" |
|
| 3479 |
+ onAction={() => {}}
|
|
| 3480 |
+ onClose={() => {}}
|
|
| 3481 |
+ /> |
|
| 3482 |
+ ), |
|
| 3483 |
+ }, |
|
| 3484 |
+ {
|
|
| 3485 |
+ label: "제목 없이 / 액션 없이 — 넘기지 않은 쪽은 그리지 않습니다 (닫기는 항상 있습니다)", |
|
| 3486 |
+ node: ( |
|
| 3487 |
+ <> |
|
| 3488 |
+ <FoxSnackbar title="제목만 있는 스낵바" actionLabel="보기" onAction={() => {}} onClose={() => {}} />
|
|
| 3489 |
+ <FoxSnackbar message="제목 없이 본문만 있는 스낵바입니다." actionLabel="보기" onAction={() => {}} onClose={() => {}} />
|
|
| 3490 |
+ <FoxSnackbar title="액션이 없는 스낵바" message="닫기만 있습니다." onClose={() => {}} />
|
|
| 3491 |
+ </> |
|
| 3492 |
+ ), |
|
| 3493 |
+ }, |
|
| 3494 |
+ {
|
|
| 3495 |
+ label: "제목이 길면 줄바꿈되고 버튼은 오른쪽에 남습니다", |
|
| 3496 |
+ node: ( |
|
| 3497 |
+ <FoxSnackbar |
|
| 3498 |
+ title="제목이 길어져 두 줄이 되어도" |
|
| 3499 |
+ message="글자 버튼과 닫기는 자리를 지킵니다. 판은 640px에서 시작하고 오른쪽 64px은 닫기 자리로 비워 둡니다." |
|
| 3500 |
+ actionLabel="자세히" |
|
| 3501 |
+ onAction={() => {}}
|
|
| 3502 |
+ onClose={() => {}}
|
|
| 3503 |
+ /> |
|
| 3504 |
+ ), |
|
| 3505 |
+ }, |
|
| 3506 |
+ ], |
|
| 3507 |
+ }, |
|
| 3508 |
+ {
|
|
| 3509 |
+ id: "fox-snackbar-list", |
|
| 3510 |
+ name: "FoxSnackbarList", |
|
| 3511 |
+ description: |
|
| 3512 |
+ "Figma 시안(snackbar-list 467:5958) 스낵바가 여러 개일 때 세로로 쌓는 묶음입니다. — 무엇이 몇 개 떠 있는지, 어떤 순서로 쌓을지는 호출부가 소유합니다. 자식에게 덮어쓰는 값이 없다는 점이 FoxTagList·FoxChipArea와 다릅니다.", |
|
| 3513 |
+ variants: [ |
|
| 3514 |
+ {
|
|
| 3515 |
+ label: "세 개가 쌓인 모습 (간격 8px)", |
|
| 3516 |
+ node: ( |
|
| 3517 |
+ <FoxSnackbarList label="알림"> |
|
| 3518 |
+ <FoxSnackbar |
|
| 3519 |
+ icon={<FoxCheckCircleIcon weight="fill" />}
|
|
| 3520 |
+ title="변경 사항을 저장했습니다" |
|
| 3521 |
+ message="3분 안에는 되돌릴 수 있습니다." |
|
| 3522 |
+ actionLabel="실행 취소" |
|
| 3523 |
+ onAction={() => {}}
|
|
| 3524 |
+ onClose={() => {}}
|
|
| 3525 |
+ /> |
|
| 3526 |
+ <FoxSnackbar |
|
| 3527 |
+ title="파일 3개를 올렸습니다" |
|
| 3528 |
+ actionLabel="보기" |
|
| 3529 |
+ onAction={() => {}}
|
|
| 3530 |
+ onClose={() => {}}
|
|
| 3531 |
+ /> |
|
| 3532 |
+ <FoxSnackbar message="제목 없이 본문만 있는 스낵바입니다." onClose={() => {}} />
|
|
| 3533 |
+ </FoxSnackbarList> |
|
| 3534 |
+ ), |
|
| 3535 |
+ }, |
|
| 3536 |
+ {
|
|
| 3537 |
+ label: "하나만 있어도 그대로 씁니다", |
|
| 3538 |
+ node: ( |
|
| 3539 |
+ <FoxSnackbarList label="알림"> |
|
| 3540 |
+ <FoxSnackbar title="하나만 떠 있는 경우" onClose={() => {}} />
|
|
| 3541 |
+ </FoxSnackbarList> |
|
| 3542 |
+ ), |
|
| 3543 |
+ }, |
|
| 3544 |
+ ], |
|
| 3545 |
+ }, |
|
| 3450 | 3546 |
]; |
+++ @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/components.scss
+++ @fox/styles/components.scss
... | ... | @@ -51,6 +51,8 @@ |
| 51 | 51 |
@use "fox-table-cell"; |
| 52 | 52 |
@use "fox-tab"; |
| 53 | 53 |
@use "fox-tab-item"; |
| 54 |
+@use "fox-snackbar"; |
|
| 55 |
+@use "fox-snackbar-list"; |
|
| 54 | 56 |
@use "fox-spinner"; |
| 55 | 57 |
@use "fox-status-indicator"; |
| 56 | 58 |
@use "fox-toast"; |
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?