@uniai-fe/uds-primitives 0.5.6 → 0.6.1

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.
Files changed (42) hide show
  1. package/README.md +25 -4
  2. package/dist/styles.css +237 -16
  3. package/package.json +1 -1
  4. package/src/components/alternate/{markup/index.tsx → Alternate.tsx} +7 -4
  5. package/src/components/alternate/index.tsx +2 -1
  6. package/src/components/alternate/layout/Button.tsx +29 -0
  7. package/src/components/alternate/layout/Container.tsx +24 -0
  8. package/src/components/alternate/layout/Contents.tsx +23 -0
  9. package/src/components/alternate/layout/Figure.tsx +24 -0
  10. package/src/components/alternate/layout/Layout.tsx +27 -0
  11. package/src/components/alternate/layout/TextButton.tsx +50 -0
  12. package/src/components/alternate/layout/Title.tsx +25 -0
  13. package/src/components/alternate/layout/index.tsx +1 -0
  14. package/src/components/alternate/styles/index.scss +3 -1
  15. package/src/components/alternate/styles/layout.scss +63 -0
  16. package/src/components/alternate/styles/{alternate.scss → unit.scss} +16 -16
  17. package/src/components/alternate/styles/variables.scss +30 -0
  18. package/src/components/alternate/types/index.ts +2 -77
  19. package/src/components/alternate/types/layout.ts +126 -0
  20. package/src/components/alternate/types/unit.ts +77 -0
  21. package/src/components/alternate/{markup → unit}/empty/Data.tsx +1 -1
  22. package/src/components/alternate/{markup → unit}/loading/Default.tsx +1 -1
  23. package/src/components/toast/img/error.svg +6 -0
  24. package/src/components/toast/img/success.svg +5 -0
  25. package/src/components/toast/img/warning.svg +5 -0
  26. package/src/components/toast/index.scss +1 -0
  27. package/src/components/toast/index.tsx +11 -0
  28. package/src/components/toast/markup/Host.tsx +74 -0
  29. package/src/components/toast/markup/Icon.tsx +15 -0
  30. package/src/components/toast/markup/Item.tsx +100 -0
  31. package/src/components/toast/markup/Text.tsx +21 -0
  32. package/src/components/toast/markup/index.tsx +16 -0
  33. package/src/components/toast/styles/index.scss +2 -0
  34. package/src/components/toast/styles/toast.scss +113 -0
  35. package/src/components/toast/styles/variables.scss +24 -0
  36. package/src/components/toast/types/index.ts +1 -0
  37. package/src/components/toast/types/internal.ts +71 -0
  38. package/src/components/toast/types/props.ts +128 -0
  39. package/src/index.scss +1 -0
  40. package/src/index.tsx +1 -0
  41. /package/src/components/alternate/{markup/Label.tsx → unit/Text.tsx} +0 -0
  42. /package/src/components/alternate/{markup → unit}/loading/Icon.tsx +0 -0
@@ -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,2 @@
1
+ @use "./variables";
2
+ @use "./toast";
@@ -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
@@ -22,4 +22,5 @@
22
22
  @use "./components/switch";
23
23
  @use "./components/tab";
24
24
  @use "./components/table";
25
+ @use "./components/toast";
25
26
  @use "./components/tooltip";
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";