@uniai-fe/uds-primitives 0.5.6 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/styles.css +131 -0
- package/package.json +1 -1
- package/src/components/toast/img/error.svg +6 -0
- package/src/components/toast/img/success.svg +5 -0
- package/src/components/toast/img/warning.svg +5 -0
- package/src/components/toast/index.scss +1 -0
- package/src/components/toast/index.tsx +11 -0
- package/src/components/toast/markup/Host.tsx +74 -0
- package/src/components/toast/markup/Icon.tsx +15 -0
- package/src/components/toast/markup/Item.tsx +100 -0
- package/src/components/toast/markup/Text.tsx +21 -0
- package/src/components/toast/markup/index.tsx +16 -0
- package/src/components/toast/styles/index.scss +2 -0
- package/src/components/toast/styles/toast.scss +113 -0
- package/src/components/toast/styles/variables.scss +24 -0
- package/src/components/toast/types/index.ts +1 -0
- package/src/components/toast/types/internal.ts +71 -0
- package/src/components/toast/types/props.ts +128 -0
- package/src/index.scss +1 -0
- package/src/index.tsx +1 -0
package/README.md
CHANGED
|
@@ -194,6 +194,15 @@ export default function Page() {
|
|
|
194
194
|
- `TableRootProps`
|
|
195
195
|
- `TableContainerProps`
|
|
196
196
|
- `TableCellProps`
|
|
197
|
+
- `Toast.Host`
|
|
198
|
+
- `Toast.Item`
|
|
199
|
+
- `ToastIcon`
|
|
200
|
+
- `ToastItemData`
|
|
201
|
+
- `ToastHostProps`
|
|
202
|
+
- `ToastItemProps`
|
|
203
|
+
- `ToastState`
|
|
204
|
+
- `ToastHorizontal`
|
|
205
|
+
- `ToastVertical`
|
|
197
206
|
- `Button.Default`
|
|
198
207
|
- `Button.Text`
|
|
199
208
|
- `Button.Rounded`
|
package/dist/styles.css
CHANGED
|
@@ -779,6 +779,26 @@
|
|
|
779
779
|
--table-td-text-size: 15px;
|
|
780
780
|
--table-td-text-weight: var(--font-body-xsmall-weight);
|
|
781
781
|
--table-td-text-line-height: var(--font-body-xsmall-line-height);
|
|
782
|
+
--toast-stack-margin: var(--spacing-padding-7);
|
|
783
|
+
--toast-stack-gap: var(--spacing-gap-3);
|
|
784
|
+
--toast-stack-z-index: 10000;
|
|
785
|
+
--toast-width: 361px;
|
|
786
|
+
--toast-background-color: var(--color-surface-heavy);
|
|
787
|
+
--toast-foreground-color: var(--color-common-100);
|
|
788
|
+
--toast-radius: var(--theme-radius-large-1);
|
|
789
|
+
--toast-padding-block: var(--spacing-padding-5);
|
|
790
|
+
--toast-padding-inline: var(--spacing-padding-7);
|
|
791
|
+
--toast-gap: var(--spacing-gap-5);
|
|
792
|
+
--toast-icon-size: 24px;
|
|
793
|
+
--toast-transition-duration: 0.6s;
|
|
794
|
+
--toast-transition-easing: cubic-bezier(0.16, 1, 0.3, 1);
|
|
795
|
+
--toast-transition-distance: -24%;
|
|
796
|
+
--toast-transition-x: 0;
|
|
797
|
+
--toast-transition-y: 0;
|
|
798
|
+
--toast-message-font-size: var(--font-body-xsmall-size);
|
|
799
|
+
--toast-message-line-height: var(--font-body-xsmall-line-height);
|
|
800
|
+
--toast-message-letter-spacing: var(--font-body-xsmall-letter-spacing);
|
|
801
|
+
--toast-message-font-weight: var(--font-body-xsmall-weight);
|
|
782
802
|
--tooltip-message-background: var(--color-cool-gray-20);
|
|
783
803
|
--tooltip-message-foreground: var(--color-common-100);
|
|
784
804
|
--tooltip-message-radius: var(--theme-radius-medium-3);
|
|
@@ -5097,6 +5117,117 @@ figure.chip {
|
|
|
5097
5117
|
|
|
5098
5118
|
|
|
5099
5119
|
|
|
5120
|
+
.toast-stack {
|
|
5121
|
+
--toast-stack-translate-x: 0;
|
|
5122
|
+
--toast-stack-translate-y: 0;
|
|
5123
|
+
position: fixed;
|
|
5124
|
+
z-index: var(--toast-stack-z-index);
|
|
5125
|
+
display: flex;
|
|
5126
|
+
flex-direction: column;
|
|
5127
|
+
gap: var(--toast-stack-gap);
|
|
5128
|
+
pointer-events: none;
|
|
5129
|
+
transform: translate(var(--toast-stack-translate-x), var(--toast-stack-translate-y));
|
|
5130
|
+
}
|
|
5131
|
+
|
|
5132
|
+
.toast-stack:where([data-x=left]) {
|
|
5133
|
+
left: var(--toast-stack-margin);
|
|
5134
|
+
--toast-transition-x: calc(var(--toast-transition-distance) * -1);
|
|
5135
|
+
}
|
|
5136
|
+
|
|
5137
|
+
.toast-stack:where([data-x=center]) {
|
|
5138
|
+
left: 50%;
|
|
5139
|
+
--toast-stack-translate-x: -50%;
|
|
5140
|
+
--toast-transition-distance: -80%;
|
|
5141
|
+
}
|
|
5142
|
+
|
|
5143
|
+
.toast-stack:where([data-x=right]) {
|
|
5144
|
+
right: var(--toast-stack-margin);
|
|
5145
|
+
--toast-transition-x: var(--toast-transition-distance);
|
|
5146
|
+
}
|
|
5147
|
+
|
|
5148
|
+
.toast-stack:where([data-y=top]) {
|
|
5149
|
+
top: var(--toast-stack-margin);
|
|
5150
|
+
}
|
|
5151
|
+
|
|
5152
|
+
.toast-stack:where([data-y=center]) {
|
|
5153
|
+
top: 50%;
|
|
5154
|
+
--toast-stack-translate-y: -50%;
|
|
5155
|
+
}
|
|
5156
|
+
|
|
5157
|
+
.toast-stack:where([data-y=bottom]) {
|
|
5158
|
+
bottom: var(--toast-stack-margin);
|
|
5159
|
+
}
|
|
5160
|
+
|
|
5161
|
+
.toast-stack:where([data-x=center][data-y=top]) {
|
|
5162
|
+
--toast-transition-y: calc(var(--toast-transition-distance) * -1);
|
|
5163
|
+
}
|
|
5164
|
+
|
|
5165
|
+
.toast-stack:where([data-x=center][data-y=bottom]) {
|
|
5166
|
+
--toast-transition-y: var(--toast-transition-distance);
|
|
5167
|
+
}
|
|
5168
|
+
|
|
5169
|
+
.toast {
|
|
5170
|
+
display: flex;
|
|
5171
|
+
align-items: center;
|
|
5172
|
+
width: min(var(--toast-width), 100vw - var(--toast-stack-margin) * 2);
|
|
5173
|
+
box-sizing: border-box;
|
|
5174
|
+
gap: var(--toast-gap);
|
|
5175
|
+
overflow: hidden;
|
|
5176
|
+
padding: var(--toast-padding-block) var(--toast-padding-inline);
|
|
5177
|
+
border-radius: var(--toast-radius);
|
|
5178
|
+
background-color: var(--toast-background-color);
|
|
5179
|
+
opacity: 0;
|
|
5180
|
+
pointer-events: auto;
|
|
5181
|
+
transform: translate(var(--toast-transition-x), var(--toast-transition-y));
|
|
5182
|
+
transition: opacity var(--toast-transition-duration) var(--toast-transition-easing), transform var(--toast-transition-duration) var(--toast-transition-easing);
|
|
5183
|
+
}
|
|
5184
|
+
|
|
5185
|
+
.toast:where([data-phase=open]) {
|
|
5186
|
+
opacity: 1;
|
|
5187
|
+
transform: translateY(0);
|
|
5188
|
+
}
|
|
5189
|
+
|
|
5190
|
+
.toast-icon {
|
|
5191
|
+
display: flex;
|
|
5192
|
+
align-items: center;
|
|
5193
|
+
justify-content: center;
|
|
5194
|
+
flex-shrink: 0;
|
|
5195
|
+
width: var(--toast-icon-size);
|
|
5196
|
+
height: var(--toast-icon-size);
|
|
5197
|
+
margin: 0;
|
|
5198
|
+
}
|
|
5199
|
+
|
|
5200
|
+
.toast-icon svg {
|
|
5201
|
+
display: block;
|
|
5202
|
+
width: 100%;
|
|
5203
|
+
height: 100%;
|
|
5204
|
+
}
|
|
5205
|
+
|
|
5206
|
+
.toast-content {
|
|
5207
|
+
display: flex;
|
|
5208
|
+
flex: 1;
|
|
5209
|
+
min-width: 0;
|
|
5210
|
+
flex-direction: column;
|
|
5211
|
+
}
|
|
5212
|
+
|
|
5213
|
+
.toast-text {
|
|
5214
|
+
overflow: hidden;
|
|
5215
|
+
margin: 0;
|
|
5216
|
+
color: var(--toast-foreground-color);
|
|
5217
|
+
font-size: var(--toast-message-font-size);
|
|
5218
|
+
line-height: var(--toast-message-line-height);
|
|
5219
|
+
letter-spacing: var(--toast-message-letter-spacing);
|
|
5220
|
+
font-weight: var(--toast-message-font-weight);
|
|
5221
|
+
text-overflow: ellipsis;
|
|
5222
|
+
white-space: nowrap;
|
|
5223
|
+
}
|
|
5224
|
+
|
|
5225
|
+
.toast-description {
|
|
5226
|
+
opacity: 0.82;
|
|
5227
|
+
}
|
|
5228
|
+
|
|
5229
|
+
|
|
5230
|
+
|
|
5100
5231
|
.tooltip-trigger {
|
|
5101
5232
|
display: inline-flex;
|
|
5102
5233
|
align-items: center;
|
package/package.json
CHANGED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<circle cx="12" cy="12" r="10" fill="#313235"/>
|
|
3
|
+
<circle cx="12" cy="12" r="10" fill="#DA1D0B"/>
|
|
4
|
+
<path d="M12 15C12.5523 15 13 15.4477 13 16C13 16.5523 12.5523 17 12 17C11.4477 17 11 16.5523 11 16C11 15.4477 11.4477 15 12 15Z" fill="white"/>
|
|
5
|
+
<path d="M12 7.2002C12.4418 7.2002 12.7998 7.55817 12.7998 8V13C12.7998 13.4418 12.4418 13.7998 12 13.7998C11.5582 13.7998 11.2002 13.4418 11.2002 13V8C11.2002 7.55817 11.5582 7.2002 12 7.2002Z" fill="white"/>
|
|
6
|
+
</svg>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<circle cx="12" cy="12" r="10" fill="#313235"/>
|
|
3
|
+
<circle cx="12" cy="12" r="10" fill="#1AB24D"/>
|
|
4
|
+
<path d="M7.19152 11.8688C6.8791 11.5564 6.8791 11.0501 7.19152 10.7377C7.50394 10.4253 8.0102 10.4253 8.32262 10.7377L10.9391 13.3541L15.6768 8.61636C15.9892 8.30394 16.4955 8.30394 16.8079 8.61636C17.1203 8.92877 17.1203 9.43503 16.8079 9.74745L11.5046 15.0508C11.1922 15.3632 10.6859 15.3632 10.3735 15.0508L7.19152 11.8688Z" fill="white"/>
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M2.6303 18.0362L10.2478 4.18631C11.0076 2.80476 12.9928 2.80476 13.7526 4.18631L21.3701 18.0362C22.1032 19.3691 21.1389 21.0001 19.6176 21.0001H4.38273C2.86153 21.0001 1.8972 19.3691 2.6303 18.0362Z" fill="#F2CC0D"/>
|
|
3
|
+
<path d="M12 16C12.5523 16 13 16.4477 13 17C13 17.5523 12.5523 18 12 18C11.4477 18 11 17.5523 11 17C11 16.4477 11.4477 16 12 16Z" fill="#313235"/>
|
|
4
|
+
<path d="M12 8.2002C12.4418 8.2002 12.7998 8.55817 12.7998 9V14C12.7998 14.4418 12.4418 14.7998 12 14.7998C11.5582 14.7998 11.2002 14.4418 11.2002 14V9C11.2002 8.55817 11.5582 8.2002 12 8.2002Z" fill="#313235"/>
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@use "./styles";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toast; semantic feedback toast 카테고리 배럴
|
|
3
|
+
* @desc
|
|
4
|
+
* - `Toast.Host`: 트리거 로직이 만든 item data를 위치별 stack으로 렌더링한다.
|
|
5
|
+
* - `Toast.Item`: state icon/content/timer close를 담당하는 개별 toast다.
|
|
6
|
+
* - `ToastIcon`, `ToastItemData`, `ToastState`: public contract 도구다.
|
|
7
|
+
*/
|
|
8
|
+
import "./index.scss";
|
|
9
|
+
|
|
10
|
+
export * from "./markup";
|
|
11
|
+
export type * from "./types";
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Fragment } from "react";
|
|
2
|
+
|
|
3
|
+
import type { ToastHostProps, ToastItemData } from "../types";
|
|
4
|
+
import type { ToastStackGroup, ToastStackStyle } from "../types/internal";
|
|
5
|
+
import { ToastItem } from "./Item";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Toast Host; x/y/margin 기준으로 Toast stack을 렌더링하는 Host
|
|
9
|
+
* @component
|
|
10
|
+
* @param {ToastHostProps} props
|
|
11
|
+
* @param {ToastItemData[]} props.items Toast item 데이터 목록
|
|
12
|
+
* @param {(toastKey: string) => void} props.onClose 닫힘 요청 핸들러
|
|
13
|
+
* @example
|
|
14
|
+
* <Toast.Host items={toastItems} onClose={onToastClose} />
|
|
15
|
+
*/
|
|
16
|
+
export function ToastHost({ items, onClose }: ToastHostProps) {
|
|
17
|
+
const stackGroups = items.reduce<ToastStackGroup[]>((groups, item) => {
|
|
18
|
+
const stackKey = `${item.y}-${item.x}-${String(item.margin)}`;
|
|
19
|
+
const nextGroup = groups.find(group => group.stackKey === stackKey);
|
|
20
|
+
|
|
21
|
+
if (nextGroup) {
|
|
22
|
+
nextGroup.items.push(item);
|
|
23
|
+
return groups;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
groups.push({
|
|
27
|
+
stackKey,
|
|
28
|
+
x: item.x,
|
|
29
|
+
y: item.y,
|
|
30
|
+
margin: item.margin,
|
|
31
|
+
items: [item],
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return groups;
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
if (!stackGroups.length) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Fragment>
|
|
43
|
+
{stackGroups.map(group => {
|
|
44
|
+
const stackMargin =
|
|
45
|
+
typeof group.margin === "number" ? `${group.margin}px` : group.margin;
|
|
46
|
+
const stackStyle: ToastStackStyle = {
|
|
47
|
+
"--toast-stack-margin": stackMargin,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
key={group.stackKey}
|
|
53
|
+
className="toast-stack"
|
|
54
|
+
data-x={group.x}
|
|
55
|
+
data-y={group.y}
|
|
56
|
+
style={stackStyle}
|
|
57
|
+
>
|
|
58
|
+
{group.items.map((item: ToastItemData) => (
|
|
59
|
+
<ToastItem
|
|
60
|
+
key={item.toastKey}
|
|
61
|
+
toastKey={item.toastKey}
|
|
62
|
+
duration={item.duration}
|
|
63
|
+
state={item.state}
|
|
64
|
+
message={item.message}
|
|
65
|
+
description={item.description}
|
|
66
|
+
onClose={onClose}
|
|
67
|
+
/>
|
|
68
|
+
))}
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
})}
|
|
72
|
+
</Fragment>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import ErrorIcon from "../img/error.svg";
|
|
2
|
+
import SuccessIcon from "../img/success.svg";
|
|
3
|
+
import WarningIcon from "../img/warning.svg";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Toast Icon Set; 상태별 아이콘 컴포넌트 맵
|
|
7
|
+
* @desc
|
|
8
|
+
* - standard는 Figma 기준 아이콘을 렌더링하지 않는다.
|
|
9
|
+
* - success/warning/error는 제공된 state SVG를 사용한다.
|
|
10
|
+
*/
|
|
11
|
+
export const ToastIcon = {
|
|
12
|
+
success: SuccessIcon,
|
|
13
|
+
warning: WarningIcon,
|
|
14
|
+
error: ErrorIcon,
|
|
15
|
+
} as const;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import clsx from "clsx";
|
|
2
|
+
import { createElement, forwardRef, useEffect, useState } from "react";
|
|
3
|
+
|
|
4
|
+
import type { ToastItemProps } from "../types";
|
|
5
|
+
import type { ToastItemStyle } from "../types/internal";
|
|
6
|
+
import { ToastIcon } from "./Icon";
|
|
7
|
+
import { ToastText } from "./Text";
|
|
8
|
+
|
|
9
|
+
const TOAST_TRANSITION_MS = 600;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Toast Item; 개별 Toast 메시지 렌더러
|
|
13
|
+
* @component
|
|
14
|
+
* @param {ToastItemProps} props
|
|
15
|
+
* @param {string} props.toastKey 트리거 지점을 드러내는 semantic key
|
|
16
|
+
* @param {number} props.duration 자동 닫힘까지의 시간(ms)
|
|
17
|
+
* @param {"standard" | "success" | "warning" | "error"} props.state Toast 피드백 성격
|
|
18
|
+
* @param {React.ReactNode} props.message 한 줄 메시지 콘텐츠
|
|
19
|
+
* @param {React.ReactNode} [props.description] 선택 보조 콘텐츠
|
|
20
|
+
* @param {(toastKey: string) => void} props.onClose 닫힘 요청 핸들러
|
|
21
|
+
* @example
|
|
22
|
+
* <Toast.Item toastKey="save-success" duration={3000} state="success" message="저장되었습니다." onClose={onClose} />
|
|
23
|
+
*/
|
|
24
|
+
const ToastItem = forwardRef<HTMLElementTagNameMap["section"], ToastItemProps>(
|
|
25
|
+
(
|
|
26
|
+
{
|
|
27
|
+
toastKey,
|
|
28
|
+
duration,
|
|
29
|
+
state,
|
|
30
|
+
message,
|
|
31
|
+
description,
|
|
32
|
+
onClose,
|
|
33
|
+
className,
|
|
34
|
+
role,
|
|
35
|
+
style,
|
|
36
|
+
...restProps
|
|
37
|
+
},
|
|
38
|
+
ref,
|
|
39
|
+
) => {
|
|
40
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
41
|
+
const toastStyle: ToastItemStyle = {
|
|
42
|
+
...style,
|
|
43
|
+
"--toast-transition-duration": `${TOAST_TRANSITION_MS}ms`,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
// 변경: mount 직후 open phase를 분리해 enter transition이 실제 렌더 프레임에서 실행되게 한다.
|
|
48
|
+
const enterFrameId = window.requestAnimationFrame(() => {
|
|
49
|
+
setIsVisible(true);
|
|
50
|
+
});
|
|
51
|
+
let exitTimerId: number | undefined;
|
|
52
|
+
|
|
53
|
+
// 변경: duration 만료 후 즉시 제거하지 않고 closed phase를 거쳐 exit transition을 보장한다.
|
|
54
|
+
const closeTimerId = window.setTimeout(() => {
|
|
55
|
+
setIsVisible(false);
|
|
56
|
+
exitTimerId = window.setTimeout(() => {
|
|
57
|
+
onClose(toastKey);
|
|
58
|
+
}, TOAST_TRANSITION_MS);
|
|
59
|
+
}, duration);
|
|
60
|
+
|
|
61
|
+
return () => {
|
|
62
|
+
window.cancelAnimationFrame(enterFrameId);
|
|
63
|
+
window.clearTimeout(closeTimerId);
|
|
64
|
+
if (typeof exitTimerId === "number") {
|
|
65
|
+
window.clearTimeout(exitTimerId);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}, [duration, onClose, toastKey]);
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<section
|
|
72
|
+
{...restProps}
|
|
73
|
+
ref={ref}
|
|
74
|
+
className={clsx("toast", className)}
|
|
75
|
+
data-phase={isVisible ? "open" : "closed"}
|
|
76
|
+
data-state={state}
|
|
77
|
+
data-toast-key={toastKey}
|
|
78
|
+
role={role ?? "status"}
|
|
79
|
+
aria-live="polite"
|
|
80
|
+
style={toastStyle}
|
|
81
|
+
>
|
|
82
|
+
{state === "standard" ? null : (
|
|
83
|
+
<figure className="toast-icon" aria-hidden="true">
|
|
84
|
+
{createElement(ToastIcon[state])}
|
|
85
|
+
</figure>
|
|
86
|
+
)}
|
|
87
|
+
<div className="toast-content">
|
|
88
|
+
<ToastText>{message}</ToastText>
|
|
89
|
+
{description ? (
|
|
90
|
+
<ToastText className="toast-description">{description}</ToastText>
|
|
91
|
+
) : null}
|
|
92
|
+
</div>
|
|
93
|
+
</section>
|
|
94
|
+
);
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
ToastItem.displayName = "Toast.Item";
|
|
99
|
+
|
|
100
|
+
export { ToastItem };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import clsx from "clsx";
|
|
2
|
+
import { Slot } from "../../slot";
|
|
3
|
+
import type { ToastTextProps } from "../types/internal";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Toast Text; Toast 텍스트 className 계약을 고정하는 전용 마크업
|
|
7
|
+
* @component
|
|
8
|
+
* @param {object} props
|
|
9
|
+
* @param {React.ReactNode} [props.children] 문자열/숫자는 p 태그로 감싸고, ReactNode는 그대로 렌더링한다.
|
|
10
|
+
* @param {string} [props.className] 텍스트 className
|
|
11
|
+
* @example
|
|
12
|
+
* <ToastText>저장되었습니다.</ToastText>
|
|
13
|
+
*/
|
|
14
|
+
export function ToastText({ children, className }: ToastTextProps) {
|
|
15
|
+
// 변경: Toast 메시지 텍스트는 Slot.Text로 위임해 string | number 래핑 규칙을 유지한다.
|
|
16
|
+
return (
|
|
17
|
+
<Slot.Text as="p" className={clsx("toast-text", className)}>
|
|
18
|
+
{children}
|
|
19
|
+
</Slot.Text>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ToastHost } from "./Host";
|
|
2
|
+
import { ToastItem } from "./Item";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Toast; semantic feedback toast namespace
|
|
6
|
+
* @desc
|
|
7
|
+
* - `Toast.Host`: item data를 x/y/margin별 stack으로 렌더링한다.
|
|
8
|
+
* - `Toast.Item`: 개별 toast content와 timer close를 담당한다.
|
|
9
|
+
*/
|
|
10
|
+
export const Toast = {
|
|
11
|
+
Host: ToastHost,
|
|
12
|
+
Item: ToastItem,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export { ToastHost, ToastItem };
|
|
16
|
+
export * from "./Icon";
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
.toast-stack {
|
|
2
|
+
--toast-stack-translate-x: 0;
|
|
3
|
+
--toast-stack-translate-y: 0;
|
|
4
|
+
position: fixed;
|
|
5
|
+
z-index: var(--toast-stack-z-index);
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
gap: var(--toast-stack-gap);
|
|
9
|
+
pointer-events: none;
|
|
10
|
+
transform: translate(
|
|
11
|
+
var(--toast-stack-translate-x),
|
|
12
|
+
var(--toast-stack-translate-y)
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.toast-stack:where([data-x="left"]) {
|
|
17
|
+
left: var(--toast-stack-margin);
|
|
18
|
+
--toast-transition-x: calc(var(--toast-transition-distance) * -1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.toast-stack:where([data-x="center"]) {
|
|
22
|
+
left: 50%;
|
|
23
|
+
--toast-stack-translate-x: -50%;
|
|
24
|
+
--toast-transition-distance: -80%;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.toast-stack:where([data-x="right"]) {
|
|
28
|
+
right: var(--toast-stack-margin);
|
|
29
|
+
--toast-transition-x: var(--toast-transition-distance);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.toast-stack:where([data-y="top"]) {
|
|
33
|
+
top: var(--toast-stack-margin);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.toast-stack:where([data-y="center"]) {
|
|
37
|
+
top: 50%;
|
|
38
|
+
--toast-stack-translate-y: -50%;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.toast-stack:where([data-y="bottom"]) {
|
|
42
|
+
bottom: var(--toast-stack-margin);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.toast-stack:where([data-x="center"][data-y="top"]) {
|
|
46
|
+
--toast-transition-y: calc(var(--toast-transition-distance) * -1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.toast-stack:where([data-x="center"][data-y="bottom"]) {
|
|
50
|
+
--toast-transition-y: var(--toast-transition-distance);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.toast {
|
|
54
|
+
display: flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
width: min(var(--toast-width), calc(100vw - (var(--toast-stack-margin) * 2)));
|
|
57
|
+
box-sizing: border-box;
|
|
58
|
+
gap: var(--toast-gap);
|
|
59
|
+
overflow: hidden;
|
|
60
|
+
padding: var(--toast-padding-block) var(--toast-padding-inline);
|
|
61
|
+
border-radius: var(--toast-radius);
|
|
62
|
+
background-color: var(--toast-background-color);
|
|
63
|
+
opacity: 0;
|
|
64
|
+
pointer-events: auto;
|
|
65
|
+
transform: translate(var(--toast-transition-x), var(--toast-transition-y));
|
|
66
|
+
transition:
|
|
67
|
+
opacity var(--toast-transition-duration) var(--toast-transition-easing),
|
|
68
|
+
transform var(--toast-transition-duration) var(--toast-transition-easing);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.toast:where([data-phase="open"]) {
|
|
72
|
+
opacity: 1;
|
|
73
|
+
transform: translateY(0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.toast-icon {
|
|
77
|
+
display: flex;
|
|
78
|
+
align-items: center;
|
|
79
|
+
justify-content: center;
|
|
80
|
+
flex-shrink: 0;
|
|
81
|
+
width: var(--toast-icon-size);
|
|
82
|
+
height: var(--toast-icon-size);
|
|
83
|
+
margin: 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.toast-icon svg {
|
|
87
|
+
display: block;
|
|
88
|
+
width: 100%;
|
|
89
|
+
height: 100%;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.toast-content {
|
|
93
|
+
display: flex;
|
|
94
|
+
flex: 1;
|
|
95
|
+
min-width: 0;
|
|
96
|
+
flex-direction: column;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.toast-text {
|
|
100
|
+
overflow: hidden;
|
|
101
|
+
margin: 0;
|
|
102
|
+
color: var(--toast-foreground-color);
|
|
103
|
+
font-size: var(--toast-message-font-size);
|
|
104
|
+
line-height: var(--toast-message-line-height);
|
|
105
|
+
letter-spacing: var(--toast-message-letter-spacing);
|
|
106
|
+
font-weight: var(--toast-message-font-weight);
|
|
107
|
+
text-overflow: ellipsis;
|
|
108
|
+
white-space: nowrap;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.toast-description {
|
|
112
|
+
opacity: 0.82;
|
|
113
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--toast-stack-margin: var(--spacing-padding-7);
|
|
3
|
+
--toast-stack-gap: var(--spacing-gap-3);
|
|
4
|
+
--toast-stack-z-index: 10000;
|
|
5
|
+
|
|
6
|
+
--toast-width: 361px;
|
|
7
|
+
--toast-background-color: var(--color-surface-heavy);
|
|
8
|
+
--toast-foreground-color: var(--color-common-100);
|
|
9
|
+
--toast-radius: var(--theme-radius-large-1);
|
|
10
|
+
--toast-padding-block: var(--spacing-padding-5);
|
|
11
|
+
--toast-padding-inline: var(--spacing-padding-7);
|
|
12
|
+
--toast-gap: var(--spacing-gap-5);
|
|
13
|
+
--toast-icon-size: 24px;
|
|
14
|
+
--toast-transition-duration: 0.6s;
|
|
15
|
+
--toast-transition-easing: cubic-bezier(0.16, 1, 0.3, 1);
|
|
16
|
+
--toast-transition-distance: -24%;
|
|
17
|
+
--toast-transition-x: 0;
|
|
18
|
+
--toast-transition-y: 0;
|
|
19
|
+
|
|
20
|
+
--toast-message-font-size: var(--font-body-xsmall-size);
|
|
21
|
+
--toast-message-line-height: var(--font-body-xsmall-line-height);
|
|
22
|
+
--toast-message-letter-spacing: var(--font-body-xsmall-letter-spacing);
|
|
23
|
+
--toast-message-font-weight: var(--font-body-xsmall-weight);
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type * from "./props";
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import type { ToastHorizontal, ToastItemData, ToastVertical } from "./props";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Toast text props; Toast 메시지 텍스트 렌더링 props
|
|
6
|
+
* @property {ReactNode} [children] 문자열/숫자는 p 태그로 감싸고, ReactNode는 그대로 렌더링한다.
|
|
7
|
+
* @property {string} [className] 텍스트 className
|
|
8
|
+
*/
|
|
9
|
+
export interface ToastTextProps {
|
|
10
|
+
/**
|
|
11
|
+
* 문자열/숫자는 p 태그로 감싸고, ReactNode는 그대로 렌더링한다.
|
|
12
|
+
*/
|
|
13
|
+
children?: ReactNode;
|
|
14
|
+
/**
|
|
15
|
+
* 텍스트 className
|
|
16
|
+
*/
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Toast stack group; Host 내부 placement bucket
|
|
22
|
+
* @property {string} stackKey x/y/margin 조합으로 만든 stack key
|
|
23
|
+
* @property {"left" | "center" | "right"} x Toast stack 가로 위치
|
|
24
|
+
* @property {"top" | "center" | "bottom"} y Toast stack 세로 위치
|
|
25
|
+
* @property {number | string} margin page frame 기준 위치 간격
|
|
26
|
+
* @property {ToastItemData[]} items 같은 위치에 stack될 Toast 목록
|
|
27
|
+
*/
|
|
28
|
+
export interface ToastStackGroup {
|
|
29
|
+
/**
|
|
30
|
+
* x/y/margin 조합으로 만든 stack key
|
|
31
|
+
*/
|
|
32
|
+
stackKey: string;
|
|
33
|
+
/**
|
|
34
|
+
* Toast stack 가로 위치
|
|
35
|
+
*/
|
|
36
|
+
x: ToastHorizontal;
|
|
37
|
+
/**
|
|
38
|
+
* Toast stack 세로 위치
|
|
39
|
+
*/
|
|
40
|
+
y: ToastVertical;
|
|
41
|
+
/**
|
|
42
|
+
* page frame 기준 위치 간격
|
|
43
|
+
*/
|
|
44
|
+
margin: number | string;
|
|
45
|
+
/**
|
|
46
|
+
* 같은 위치에 stack될 Toast 목록
|
|
47
|
+
*/
|
|
48
|
+
items: ToastItemData[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Toast stack style; 런타임 margin CSS 변수 주입 타입
|
|
53
|
+
* @property {string} --toast-stack-margin page frame 기준 위치 간격 CSS 값
|
|
54
|
+
*/
|
|
55
|
+
export interface ToastStackStyle extends CSSProperties {
|
|
56
|
+
/**
|
|
57
|
+
* page frame 기준 위치 간격 CSS 값
|
|
58
|
+
*/
|
|
59
|
+
["--toast-stack-margin"]: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Toast item style; transition duration CSS 변수 주입 타입
|
|
64
|
+
* @property {string} --toast-transition-duration Toast enter/exit transition 시간 CSS 값
|
|
65
|
+
*/
|
|
66
|
+
export interface ToastItemStyle extends CSSProperties {
|
|
67
|
+
/**
|
|
68
|
+
* Toast enter/exit transition 시간 CSS 값
|
|
69
|
+
*/
|
|
70
|
+
["--toast-transition-duration"]: string;
|
|
71
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Toast state; Toast 피드백 성격 축
|
|
5
|
+
* @typedef {"standard" | "success" | "warning" | "error"} ToastState
|
|
6
|
+
*/
|
|
7
|
+
export type ToastState = "standard" | "success" | "warning" | "error";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Toast horizontal position; Toast stack 가로 위치 축
|
|
11
|
+
* @typedef {"left" | "center" | "right"} ToastHorizontal
|
|
12
|
+
*/
|
|
13
|
+
export type ToastHorizontal = "left" | "center" | "right";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Toast vertical position; Toast stack 세로 위치 축
|
|
17
|
+
* @typedef {"top" | "center" | "bottom"} ToastVertical
|
|
18
|
+
*/
|
|
19
|
+
export type ToastVertical = "top" | "center" | "bottom";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Toast item data; 트리거 로직이 생성하는 Toast 렌더링 데이터
|
|
23
|
+
* @property {string} toastKey 트리거 지점을 드러내는 semantic key
|
|
24
|
+
* @property {"left" | "center" | "right"} x Toast stack 가로 위치
|
|
25
|
+
* @property {"top" | "center" | "bottom"} y Toast stack 세로 위치
|
|
26
|
+
* @property {number | string} margin page frame 기준 위치 간격
|
|
27
|
+
* @property {number} duration 자동 닫힘까지의 시간(ms)
|
|
28
|
+
* @property {"standard" | "success" | "warning" | "error"} state Toast 피드백 성격
|
|
29
|
+
* @property {ReactNode} message 한 줄 메시지 콘텐츠
|
|
30
|
+
* @property {ReactNode} [description] 선택 보조 콘텐츠
|
|
31
|
+
*/
|
|
32
|
+
export interface ToastItemData {
|
|
33
|
+
/**
|
|
34
|
+
* 트리거 지점을 드러내는 semantic key
|
|
35
|
+
*/
|
|
36
|
+
toastKey: string;
|
|
37
|
+
/**
|
|
38
|
+
* Toast stack 가로 위치
|
|
39
|
+
*/
|
|
40
|
+
x: ToastHorizontal;
|
|
41
|
+
/**
|
|
42
|
+
* Toast stack 세로 위치
|
|
43
|
+
*/
|
|
44
|
+
y: ToastVertical;
|
|
45
|
+
/**
|
|
46
|
+
* page frame 기준 위치 간격
|
|
47
|
+
*/
|
|
48
|
+
margin: number | string;
|
|
49
|
+
/**
|
|
50
|
+
* 자동 닫힘까지의 시간(ms)
|
|
51
|
+
*/
|
|
52
|
+
duration: number;
|
|
53
|
+
/**
|
|
54
|
+
* Toast 피드백 성격
|
|
55
|
+
*/
|
|
56
|
+
state: ToastState;
|
|
57
|
+
/**
|
|
58
|
+
* 한 줄 메시지 콘텐츠
|
|
59
|
+
*/
|
|
60
|
+
message: ReactNode;
|
|
61
|
+
/**
|
|
62
|
+
* 선택 보조 콘텐츠
|
|
63
|
+
*/
|
|
64
|
+
description?: ReactNode;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type NativeToastItemProps = Omit<
|
|
68
|
+
ComponentPropsWithoutRef<"section">,
|
|
69
|
+
"children"
|
|
70
|
+
>;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Toast item props; 개별 Toast 렌더링 props
|
|
74
|
+
* @property {string} toastKey 트리거 지점을 드러내는 semantic key
|
|
75
|
+
* @property {number} duration 자동 닫힘까지의 시간(ms)
|
|
76
|
+
* @property {"standard" | "success" | "warning" | "error"} state Toast 피드백 성격
|
|
77
|
+
* @property {ReactNode} message 한 줄 메시지 콘텐츠
|
|
78
|
+
* @property {ReactNode} [description] 선택 보조 콘텐츠
|
|
79
|
+
* @property {(toastKey: string) => void} onClose 닫힘 요청 핸들러
|
|
80
|
+
* @property {string} [className] Toast item className
|
|
81
|
+
* @see React.ComponentPropsWithoutRef<"section">
|
|
82
|
+
*/
|
|
83
|
+
export interface ToastItemProps extends NativeToastItemProps {
|
|
84
|
+
/**
|
|
85
|
+
* 트리거 지점을 드러내는 semantic key
|
|
86
|
+
*/
|
|
87
|
+
toastKey: string;
|
|
88
|
+
/**
|
|
89
|
+
* 자동 닫힘까지의 시간(ms)
|
|
90
|
+
*/
|
|
91
|
+
duration: number;
|
|
92
|
+
/**
|
|
93
|
+
* Toast 피드백 성격
|
|
94
|
+
*/
|
|
95
|
+
state: ToastState;
|
|
96
|
+
/**
|
|
97
|
+
* 한 줄 메시지 콘텐츠
|
|
98
|
+
*/
|
|
99
|
+
message: ReactNode;
|
|
100
|
+
/**
|
|
101
|
+
* 선택 보조 콘텐츠
|
|
102
|
+
*/
|
|
103
|
+
description?: ReactNode;
|
|
104
|
+
/**
|
|
105
|
+
* 닫힘 요청 핸들러
|
|
106
|
+
*/
|
|
107
|
+
onClose: (toastKey: string) => void;
|
|
108
|
+
/**
|
|
109
|
+
* Toast item className
|
|
110
|
+
*/
|
|
111
|
+
className?: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Toast host props; placement별 Toast stack 렌더링 props
|
|
116
|
+
* @property {ToastItemData[]} items Toast item 데이터 목록
|
|
117
|
+
* @property {(toastKey: string) => void} onClose 닫힘 요청 핸들러
|
|
118
|
+
*/
|
|
119
|
+
export interface ToastHostProps {
|
|
120
|
+
/**
|
|
121
|
+
* Toast item 데이터 목록
|
|
122
|
+
*/
|
|
123
|
+
items: ToastItemData[];
|
|
124
|
+
/**
|
|
125
|
+
* 닫힘 요청 핸들러
|
|
126
|
+
*/
|
|
127
|
+
onClose: (toastKey: string) => void;
|
|
128
|
+
}
|
package/src/index.scss
CHANGED
package/src/index.tsx
CHANGED
|
@@ -28,5 +28,6 @@ export * from "./components/spinner";
|
|
|
28
28
|
export * from "./components/switch";
|
|
29
29
|
export * from "./components/tab";
|
|
30
30
|
export * from "./components/table";
|
|
31
|
+
export * from "./components/toast";
|
|
31
32
|
export * from "./components/tooltip";
|
|
32
33
|
export type * from "./types";
|