atomsystem 0.1.47 → 0.1.49
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 +153 -55
- package/atom.design.md +155 -0
- package/atom.uxPattern.md +431 -0
- package/components/ui/Badge.tsx +20 -0
- package/components/ui/Button.tsx +33 -0
- package/components/ui/Card.tsx +20 -0
- package/components/ui/Input.tsx +35 -0
- package/components/ui/Modal.tsx +54 -0
- package/components/ui/index.ts +5 -0
- package/css/atom-ui.css +216 -0
- package/dist/cli.cjs +105 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +97 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +7897 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +218 -0
- package/dist/index.d.ts +218 -0
- package/dist/index.js +7845 -447
- package/dist/index.js.map +1 -0
- package/package.json +120 -81
- package/presets/default.design.md +155 -0
- package/presets/uxPattern.md +431 -0
- package/skills/atomsystem.skill.md +104 -0
- package/tokens/token.json +319 -0
- package/tokens/tokens.json +318 -976
- package/app/templates/Animations/Animation.tsx +0 -282
- package/app/templates/Badge/Badge.tsx +0 -28
- package/app/templates/Badge/badge.css +0 -28
- package/app/templates/Button/Button.tsx +0 -31
- package/app/templates/Button/button.css +0 -135
- package/app/templates/Card/Card.tsx +0 -50
- package/app/templates/Card/card.css +0 -17
- package/app/templates/CodeBlock/CodeBlock.tsx +0 -68
- package/app/templates/Collapse/Collapse.tsx +0 -207
- package/app/templates/Collapse/collapse.css +0 -24
- package/app/templates/Dropdown/Dropdown.css +0 -112
- package/app/templates/Dropdown/Dropdown.tsx +0 -103
- package/app/templates/Form/Form.tsx +0 -289
- package/app/templates/Form/form.css +0 -115
- package/app/templates/Modal/Modal.tsx +0 -71
- package/app/templates/Modal/modal.css +0 -98
- package/app/templates/Navigation/Navigation.tsx +0 -169
- package/app/templates/Navigation/navigation.css +0 -59
- package/app/templates/Pagination/Pagination.tsx +0 -104
- package/app/templates/Pagination/pagination.css +0 -66
- package/app/templates/Sidebar/Sidebar.tsx +0 -139
- package/app/templates/Sidebar/sidebar.css +0 -4
- package/app/templates/TOC/TOC.tsx +0 -165
- package/app/templates/Tab/Tab.tsx +0 -117
- package/app/templates/Tab/tab.css +0 -56
- package/app/templates/Table/Table.tsx +0 -54
- package/app/templates/Table/table.css +0 -58
- package/app/templates/Theme/Theme.tsx +0 -121
- package/app/templates/Toast/Toast.tsx +0 -106
- package/app/templates/Toast/toast.css +0 -69
- package/build/build-tokens.js +0 -223
- package/build/css/_variables.css +0 -251
- package/build/typescript/theme.ts +0 -6
- package/css/text.css +0 -18
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useRef, useState } from "react";
|
|
4
|
-
import gsap from "gsap";
|
|
5
|
-
import { useGSAP } from "@gsap/react";
|
|
6
|
-
|
|
7
|
-
const Tab = ({
|
|
8
|
-
tabs,
|
|
9
|
-
tabClass = "tab",
|
|
10
|
-
tabNavItemClass = "tab-nav-item",
|
|
11
|
-
tabNavItemActiveClass = "tab-nav-item-active",
|
|
12
|
-
tabContents,
|
|
13
|
-
tabContentClass = "tab-content",
|
|
14
|
-
tabContentItem = "tab-content-item",
|
|
15
|
-
expectionValue,
|
|
16
|
-
expectionLayout,
|
|
17
|
-
}: {
|
|
18
|
-
/**탭 네비게이션을 정의내립니다.
|
|
19
|
-
*/
|
|
20
|
-
tabs: Array<{
|
|
21
|
-
/** id는 탭 메뉴의 고유 값입니다. 고유 값을 통해 Active 상태 값을 관리합니다.*/
|
|
22
|
-
id: string;
|
|
23
|
-
/** target은 배열로 제공되며, 타겟의 배열과 동일한 내용을 tabContent로 적용됩니다.*/
|
|
24
|
-
target: string[];
|
|
25
|
-
/** 탭 메뉴의 ReactNode 내용을 받습니다.*/
|
|
26
|
-
tabItem: React.ReactNode;
|
|
27
|
-
}>;
|
|
28
|
-
/** 탭 메뉴의 tailwinds css 스타일 지정을 위한 class를 받습니다.*/
|
|
29
|
-
tabClass?: string;
|
|
30
|
-
/** 탭 메뉴의 각각의 tailwinds css 스타일 지정을 위한 class를 받습니다.*/
|
|
31
|
-
tabNavItemClass?: string;
|
|
32
|
-
/** 탭 메뉴의 활성화 상태의 tailwinds css 스타일 지정을 위한 class를 받습니다.*/
|
|
33
|
-
tabNavItemActiveClass?: string;
|
|
34
|
-
/** 탭 메뉴의 콘텐츠 값을 정의내립니다.*/
|
|
35
|
-
tabContents: Array<{
|
|
36
|
-
/** 고유 ID 값입니다.*/
|
|
37
|
-
id: string;
|
|
38
|
-
/** 태그 값으로 tabs target에서 선택된 내용만 화면에 제공됩니다.*/
|
|
39
|
-
tag: string;
|
|
40
|
-
/** 탭 메뉴의 콘텐츠 ReactNode 정보를 받습니다.*/
|
|
41
|
-
content: React.ReactNode;
|
|
42
|
-
}>;
|
|
43
|
-
/** 탭 메뉴의 콘텐츠 레이아웃을 위한한 tailwinds css 스타일 지정을 위한 class를 받습니다. */
|
|
44
|
-
tabContentClass?: string;
|
|
45
|
-
/** 탭 메뉴의 콘텐츠의 각 개별 요소를 위한 tailwinds css 스타일 지정을 위한 class를 받습니다. */
|
|
46
|
-
tabContentItem?: string;
|
|
47
|
-
/** 예외 레이아웃을 사용할 콘텐츠의 tag값을 입력받습니다. */
|
|
48
|
-
expectionValue?: string;
|
|
49
|
-
/** 예외 레이아웃에 사용하기 위한 tailwinds css 스타일 지정을 위한 class를 받습니다. */
|
|
50
|
-
expectionLayout?: string;
|
|
51
|
-
}) => {
|
|
52
|
-
const [activeContent, setActiveContent] = useState(tabs[0].target);
|
|
53
|
-
const [activeTab, setActiveTab] = useState(tabs[0].id);
|
|
54
|
-
const tabContentRef = useRef<HTMLDivElement>(null);
|
|
55
|
-
|
|
56
|
-
const handleTabActive = (target: string[], id: string) => {
|
|
57
|
-
setActiveContent(target);
|
|
58
|
-
setActiveTab(id);
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
// 👇 activeContent가 변경된 이후에 실행
|
|
62
|
-
useGSAP(() => {
|
|
63
|
-
if (tabContentRef.current) {
|
|
64
|
-
gsap.fromTo(
|
|
65
|
-
tabContentRef.current.children,
|
|
66
|
-
{ y: 50, opacity: 0 },
|
|
67
|
-
{
|
|
68
|
-
y: 0,
|
|
69
|
-
opacity: 1,
|
|
70
|
-
stagger: 0.1,
|
|
71
|
-
duration: 0.6,
|
|
72
|
-
ease: "power4.out",
|
|
73
|
-
}
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
}, [activeContent]);
|
|
77
|
-
|
|
78
|
-
return (
|
|
79
|
-
<div className="flex flex-col w-full">
|
|
80
|
-
<div className={tabClass}>
|
|
81
|
-
{tabs.map((tab) => {
|
|
82
|
-
return (
|
|
83
|
-
<button
|
|
84
|
-
key={tab.id}
|
|
85
|
-
id={tab.id}
|
|
86
|
-
onClick={() => handleTabActive(tab.target, tab.id)}
|
|
87
|
-
className={`${tabNavItemClass} ${
|
|
88
|
-
activeTab == tab.id
|
|
89
|
-
? tabNavItemActiveClass
|
|
90
|
-
: ""
|
|
91
|
-
}`}
|
|
92
|
-
>
|
|
93
|
-
{tab.tabItem}
|
|
94
|
-
</button>
|
|
95
|
-
);
|
|
96
|
-
})}
|
|
97
|
-
</div>
|
|
98
|
-
<div ref={tabContentRef} className={tabContentClass}>
|
|
99
|
-
{tabContents.map((tabContent) => {
|
|
100
|
-
return activeContent.includes(tabContent.tag) ? (
|
|
101
|
-
<div
|
|
102
|
-
key={tabContent.id}
|
|
103
|
-
id={tabContent.tag}
|
|
104
|
-
className={`${tabContentItem} ${
|
|
105
|
-
activeTab == expectionValue ? expectionLayout : ""
|
|
106
|
-
}`}
|
|
107
|
-
>
|
|
108
|
-
{tabContent.content}
|
|
109
|
-
</div>
|
|
110
|
-
) : null;
|
|
111
|
-
})}
|
|
112
|
-
</div>
|
|
113
|
-
</div>
|
|
114
|
-
);
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
export default Tab;
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
.tab {
|
|
2
|
-
display: flex;
|
|
3
|
-
width: 100%;
|
|
4
|
-
background-color: var(--color-bg-wrapper);
|
|
5
|
-
border-bottom: 1px solid var(--color-border-standard);
|
|
6
|
-
position: relative;
|
|
7
|
-
z-index: 0;
|
|
8
|
-
height: 57px;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
.tab-nav-item {
|
|
12
|
-
padding: var(--spacing-common-x);
|
|
13
|
-
cursor: pointer;
|
|
14
|
-
border: 1px solid transparent; /* reserve space for active state border */
|
|
15
|
-
border-bottom: none;
|
|
16
|
-
background-color: transparent;
|
|
17
|
-
color: var(--color-text-standard);
|
|
18
|
-
transition: all 0.2s ease-in-out;
|
|
19
|
-
display: flex;
|
|
20
|
-
align-items: center;
|
|
21
|
-
justify-content: center;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
.tab-nav-item:hover {
|
|
25
|
-
background-color: var(--color-bg-card);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
.tab-nav-item-active {
|
|
29
|
-
background-color: var(--color-bg-card);
|
|
30
|
-
border: 1px solid var(--color-border-standard);
|
|
31
|
-
border-bottom: 1px solid var(--color-bg-card);
|
|
32
|
-
position: relative;
|
|
33
|
-
z-index: 10;
|
|
34
|
-
color: var(--color-primary);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
.tab-content {
|
|
38
|
-
background-color: var(--color-bg-card);
|
|
39
|
-
padding: var(--spacing-common-x);
|
|
40
|
-
border: 1px solid var(--color-border-standard);
|
|
41
|
-
border-top: 0;
|
|
42
|
-
margin-top: -1px; /* Overlap the top line of content with the bottom line of tab to allow the active tab to connect */
|
|
43
|
-
position: relative;
|
|
44
|
-
z-index: 1;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
.tab-content-item {
|
|
48
|
-
color: var(--color-text-standard);
|
|
49
|
-
width: 100%;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
@media (min-width: 640px) {
|
|
53
|
-
.tab-content-item {
|
|
54
|
-
width: auto;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/** 테이블에 표시할 데이터 배열. 각 객체는 하나의 행을 나타냅니다.
|
|
2
|
-
* 객체의 키는 테이블 헤더가 되고, 값은 해당 셀의 내용이 됩니다.
|
|
3
|
-
*/
|
|
4
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5
|
-
const Table = ({ tableData }: { tableData: Record<string, any>[] }) => {
|
|
6
|
-
// tableData가 제공되지 않았거나 비어있는 경우, 데이터 없음 메시지를 표시합니다.
|
|
7
|
-
if (!tableData || tableData.length === 0) {
|
|
8
|
-
return (
|
|
9
|
-
<div className="table-empty">
|
|
10
|
-
<p>표시할 데이터가 없습니다.</p>
|
|
11
|
-
</div>
|
|
12
|
-
);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const headers = Object.keys(tableData[0]);
|
|
16
|
-
|
|
17
|
-
return (
|
|
18
|
-
<div className="table-container">
|
|
19
|
-
<table className="table">
|
|
20
|
-
<thead className="table-header">
|
|
21
|
-
<tr>
|
|
22
|
-
{/* 헤더 배열을 순회하며 각 헤더에 대한 <th>태그를 생성합니다. */}
|
|
23
|
-
{headers.map((header, index) => {
|
|
24
|
-
return (
|
|
25
|
-
<th key={index}>
|
|
26
|
-
{header}
|
|
27
|
-
</th>
|
|
28
|
-
);
|
|
29
|
-
})}
|
|
30
|
-
</tr>
|
|
31
|
-
</thead>
|
|
32
|
-
{/* 테이블 바디 부분 */}
|
|
33
|
-
<tbody className="table-body">
|
|
34
|
-
{/* tableData 배열을 순회하며 각 데이터 객체(행)에 대한 <tr> 태그를 생성합니다. */}
|
|
35
|
-
{tableData.map((row, rowIndex) => {
|
|
36
|
-
return (
|
|
37
|
-
<tr key={rowIndex} className="table-row">
|
|
38
|
-
{/* 각 행 내에서 헤더 배열을 다시 순회하며 해당 헤더에 맞는 셀(<td>)을 생성합니다. */}
|
|
39
|
-
{headers.map((header, colIndex) => (
|
|
40
|
-
<td key={colIndex} className="table-cell">
|
|
41
|
-
{/* 현재 행 객체(row)에서 해당 헤더(키)에 해당하는 값을 가져와 셀에 표시합니다. */}
|
|
42
|
-
{row[header]}
|
|
43
|
-
</td>
|
|
44
|
-
))}
|
|
45
|
-
</tr>
|
|
46
|
-
);
|
|
47
|
-
})}
|
|
48
|
-
</tbody>
|
|
49
|
-
</table>
|
|
50
|
-
</div>
|
|
51
|
-
);
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
export default Table;
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
.table-container {
|
|
2
|
-
overflow: auto;
|
|
3
|
-
width: 100%;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
.table {
|
|
7
|
-
min-width: 100%;
|
|
8
|
-
border-collapse: collapse;
|
|
9
|
-
border-radius: var(--radius-common);
|
|
10
|
-
border: 1px solid var(--color-table-header-border);
|
|
11
|
-
overflow: hidden;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
.table-header {
|
|
15
|
-
background-color: var(--color-table-header-bg);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
.table-header th {
|
|
19
|
-
padding: var(--spacing-common-y) var(--spacing-common-x);
|
|
20
|
-
text-align: center;
|
|
21
|
-
color: var(--color-table-header-text);
|
|
22
|
-
font-weight: var(--font-weight-header);
|
|
23
|
-
font-size: var(--text-body);
|
|
24
|
-
letter-spacing: 0.05em;
|
|
25
|
-
border-bottom: 1px solid var(--color-table-header-border);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
.table-body {
|
|
29
|
-
/* Body styles if needed */
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
.table-row {
|
|
33
|
-
background-color: var(--color-table-body-bg);
|
|
34
|
-
transition: background-color 0.2s ease;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
.table-row:nth-child(even) {
|
|
38
|
-
background-color: var(--color-table-body-bgEven);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
.table-row:hover {
|
|
42
|
-
filter: brightness(0.95);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
.table-cell {
|
|
46
|
-
padding: var(--spacing-sm-y) var(--spacing-common-x);
|
|
47
|
-
color: var(--color-table-body-text);
|
|
48
|
-
font-size: var(--text-body);
|
|
49
|
-
border-bottom: 1px solid var(--color-table-body-border);
|
|
50
|
-
text-align: left;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
.table-empty {
|
|
54
|
-
padding: var(--spacing-common-x);
|
|
55
|
-
text-align: center;
|
|
56
|
-
color: var(--color-text-standard);
|
|
57
|
-
font-size: var(--text-body);
|
|
58
|
-
}
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useEffect, useState } from "react";
|
|
4
|
-
import { Radio } from "../Form/Form";
|
|
5
|
-
import { themes } from "@/build/typescript/theme";
|
|
6
|
-
import { Button } from "../Button/Button";
|
|
7
|
-
import { ModalContent } from "../Modal/Modal";
|
|
8
|
-
|
|
9
|
-
export const ThemeSelector = () => {
|
|
10
|
-
const [selectedThemes, setSelectedThemes] = useState<Record<string, string>>({});
|
|
11
|
-
const [tempThemes, setTempThemes] = useState<Record<string, string>>({});
|
|
12
|
-
const [isThemeLoading, setThemeLoading] = useState(false);
|
|
13
|
-
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
14
|
-
|
|
15
|
-
useEffect(() => {
|
|
16
|
-
const chageThemeValue = () => {
|
|
17
|
-
const newThemes: Record<string, string> = {};
|
|
18
|
-
Object.entries(themes).forEach(([group, groupThemes]) => {
|
|
19
|
-
const storedKey = group === "global" ? "theme" : `theme-${group}`;
|
|
20
|
-
const storedTheme = localStorage.getItem(storedKey) || groupThemes[0];
|
|
21
|
-
newThemes[group] = storedTheme;
|
|
22
|
-
});
|
|
23
|
-
setSelectedThemes(newThemes);
|
|
24
|
-
setTempThemes(newThemes);
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
chageThemeValue();
|
|
28
|
-
setThemeLoading(true);
|
|
29
|
-
}, []);
|
|
30
|
-
|
|
31
|
-
const handleTempThemeChange = (group: string, theme: string) => {
|
|
32
|
-
setTempThemes((prev) => ({ ...prev, [group]: theme }));
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const handleApply = () => {
|
|
36
|
-
if (typeof window !== "undefined") {
|
|
37
|
-
Object.entries(tempThemes).forEach(([group, theme]) => {
|
|
38
|
-
const storedKey = group === "global" ? "theme" : `theme-${group}`;
|
|
39
|
-
localStorage.setItem(storedKey, theme);
|
|
40
|
-
});
|
|
41
|
-
window.dispatchEvent(new Event("storage"));
|
|
42
|
-
setSelectedThemes(tempThemes);
|
|
43
|
-
setIsModalOpen(false);
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const handleOpenModal = () => {
|
|
48
|
-
setTempThemes(selectedThemes); // Reset to current applied themes
|
|
49
|
-
setIsModalOpen(true);
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
if (isThemeLoading === false) {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return (
|
|
57
|
-
<>
|
|
58
|
-
<Button type="primary" onClick={handleOpenModal} style={{ width: "100%" }}>
|
|
59
|
-
<div className="flex items-center justify-center gap-2">
|
|
60
|
-
<span className="material-symbols-outlined" style={{ fontSize: "18px" }}>palette</span>
|
|
61
|
-
<span>테마 설정</span>
|
|
62
|
-
</div>
|
|
63
|
-
</Button>
|
|
64
|
-
<ModalContent
|
|
65
|
-
isOpen={isModalOpen}
|
|
66
|
-
onRequestClose={() => setIsModalOpen(false)}
|
|
67
|
-
modalTitle="테마 설정"
|
|
68
|
-
modalFooter={
|
|
69
|
-
<div className="flex justify-end gap-2 w-full mt-4">
|
|
70
|
-
<Button type="gray" onClick={() => setIsModalOpen(false)}>취소</Button>
|
|
71
|
-
<Button type="primary" onClick={handleApply}>적용</Button>
|
|
72
|
-
</div>
|
|
73
|
-
}
|
|
74
|
-
>
|
|
75
|
-
<div className="flex flex-col gap-6 p-4">
|
|
76
|
-
{Object.entries(themes).map(([group, groupThemes]) => (
|
|
77
|
-
<div key={group} className="flex flex-col gap-3">
|
|
78
|
-
<p className="font-bold capitalize">{group} Theme</p>
|
|
79
|
-
<div className="flex gap-4 flex-wrap">
|
|
80
|
-
{groupThemes.map((theme) => (
|
|
81
|
-
<Radio
|
|
82
|
-
name={`theme-${group}-modal`}
|
|
83
|
-
value={theme}
|
|
84
|
-
key={theme}
|
|
85
|
-
onChange={() => handleTempThemeChange(group, theme)}
|
|
86
|
-
checked={theme === tempThemes[group]}
|
|
87
|
-
/>
|
|
88
|
-
))}
|
|
89
|
-
</div>
|
|
90
|
-
</div>
|
|
91
|
-
))}
|
|
92
|
-
</div>
|
|
93
|
-
</ModalContent>
|
|
94
|
-
</>
|
|
95
|
-
);
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
export const ThemeProvider = () => {
|
|
99
|
-
const [isThemeLoading, setThemeLoading] = useState(false);
|
|
100
|
-
|
|
101
|
-
useEffect(() => {
|
|
102
|
-
const listenStorageChange = () => {
|
|
103
|
-
Object.entries(themes).forEach(([group, groupThemes]) => {
|
|
104
|
-
const storedKey = group === "global" ? "theme" : `theme-${group}`;
|
|
105
|
-
const currentTheme = localStorage.getItem(storedKey) || groupThemes[0];
|
|
106
|
-
|
|
107
|
-
document.documentElement.classList.remove(...groupThemes);
|
|
108
|
-
document.documentElement.classList.add(currentTheme);
|
|
109
|
-
});
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
listenStorageChange();
|
|
113
|
-
window.addEventListener("storage", listenStorageChange);
|
|
114
|
-
setThemeLoading(true);
|
|
115
|
-
return () => window.removeEventListener("storage", listenStorageChange);
|
|
116
|
-
}, []);
|
|
117
|
-
|
|
118
|
-
if (isThemeLoading === false) {
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
};
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useEffect, useState, useRef } from "react";
|
|
4
|
-
import gsap from "gsap";
|
|
5
|
-
import { useGSAP } from "@gsap/react";
|
|
6
|
-
|
|
7
|
-
gsap.registerPlugin(useGSAP);
|
|
8
|
-
|
|
9
|
-
// ✅ Toast Message 타입 정의
|
|
10
|
-
export type ToastMessage = {
|
|
11
|
-
id: string;
|
|
12
|
-
status?: "standard" | "primary" | "secondary" | "success" | "danger" | "warning" | "info" | "error";
|
|
13
|
-
message: string;
|
|
14
|
-
desc?: string;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
// ✅ 전역 메시지 저장소와 구독 시스템
|
|
18
|
-
let messages: ToastMessage[] = [];
|
|
19
|
-
let listener: ((messages: ToastMessage[]) => void) | null = null;
|
|
20
|
-
|
|
21
|
-
export const addToast = (msg: Omit<ToastMessage, "id">) => {
|
|
22
|
-
const id = Math.random().toString(36).substring(2) + Date.now().toString(36);
|
|
23
|
-
messages = [...messages, { id, ...msg }];
|
|
24
|
-
// messages 값 listener에 반영
|
|
25
|
-
listener?.(messages);
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const removeToast = (id: string) => {
|
|
29
|
-
messages = messages.filter((m) => m.id !== id);
|
|
30
|
-
listener?.(messages);
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
// 콜백으로 message value 추적적
|
|
34
|
-
const subscribeToast = (cb: (messages: ToastMessage[]) => void) => {
|
|
35
|
-
listener = cb;
|
|
36
|
-
cb(messages);
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const unsubscribeToast = () => {
|
|
40
|
-
listener = null;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
// ✅ useToast 훅 제공
|
|
44
|
-
export const useToast = () => {
|
|
45
|
-
return addToast;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
// ✅ ToastContainer 컴포넌트 (내부 ToastItem 포함)
|
|
49
|
-
export const Toast = () => {
|
|
50
|
-
const [messages, setMessages] = useState<ToastMessage[]>([]);
|
|
51
|
-
|
|
52
|
-
useEffect(() => {
|
|
53
|
-
subscribeToast(setMessages);
|
|
54
|
-
return () => unsubscribeToast();
|
|
55
|
-
}, []);
|
|
56
|
-
|
|
57
|
-
return (
|
|
58
|
-
<div className="toast-container">
|
|
59
|
-
{messages.map((message) => (
|
|
60
|
-
<ToastItem
|
|
61
|
-
key={message.id}
|
|
62
|
-
message={message}
|
|
63
|
-
remove={() => removeToast(message.id)}
|
|
64
|
-
/>
|
|
65
|
-
))}
|
|
66
|
-
</div>
|
|
67
|
-
);
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const ToastItem = ({
|
|
71
|
-
message,
|
|
72
|
-
remove,
|
|
73
|
-
}: {
|
|
74
|
-
message: ToastMessage;
|
|
75
|
-
remove: () => void;
|
|
76
|
-
}) => {
|
|
77
|
-
const elRef = useRef<HTMLDivElement>(null);
|
|
78
|
-
|
|
79
|
-
useGSAP(() => {
|
|
80
|
-
if (!elRef.current) return;
|
|
81
|
-
|
|
82
|
-
const tl = gsap.timeline({ onComplete: remove });
|
|
83
|
-
tl.from(elRef.current, {
|
|
84
|
-
opacity: 0,
|
|
85
|
-
y: 50,
|
|
86
|
-
ease: "power4.out",
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
tl.to(elRef.current, {
|
|
90
|
-
delay: 3,
|
|
91
|
-
y: 50,
|
|
92
|
-
opacity: 0,
|
|
93
|
-
ease: "power4.out",
|
|
94
|
-
});
|
|
95
|
-
}, []);
|
|
96
|
-
|
|
97
|
-
return (
|
|
98
|
-
<div
|
|
99
|
-
ref={elRef}
|
|
100
|
-
className={`toast-item ${message.status || "standard"}`}
|
|
101
|
-
>
|
|
102
|
-
<p>{message.message}</p>
|
|
103
|
-
{message.desc ? <p className="toast-desc">{message.desc}</p> : null}
|
|
104
|
-
</div>
|
|
105
|
-
);
|
|
106
|
-
};
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
.toast-container {
|
|
2
|
-
position: fixed;
|
|
3
|
-
display: flex;
|
|
4
|
-
flex-direction: column;
|
|
5
|
-
gap: var(--spacing-sm-y);
|
|
6
|
-
bottom: 24px;
|
|
7
|
-
right: 24px;
|
|
8
|
-
z-index: 50;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
.toast-item {
|
|
12
|
-
background-color: var(--toast-bg, var(--color-bg-card));
|
|
13
|
-
border: 1px solid var(--toast-border, var(--border-standard));
|
|
14
|
-
border-radius: var(--radius-common);
|
|
15
|
-
color: var(--toast-color, var(--text-standard));
|
|
16
|
-
padding: var(--spacing-common-x);
|
|
17
|
-
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
18
|
-
min-width: 250px;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/* Status Variants */
|
|
22
|
-
.toast-item.standard {
|
|
23
|
-
--toast-bg: var(--color-bg-card);
|
|
24
|
-
--toast-border: var(--border-standard);
|
|
25
|
-
--toast-color: var(--text-standard);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
.toast-item.primary {
|
|
29
|
-
--toast-bg: var(--color-primary);
|
|
30
|
-
--toast-border: var(--color-primary);
|
|
31
|
-
--toast-color: var(--color-white);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
.toast-item.secondary {
|
|
35
|
-
--toast-bg: var(--color-secondary);
|
|
36
|
-
--toast-border: var(--color-secondary);
|
|
37
|
-
--toast-color: var(--color-white);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
.toast-item.success {
|
|
41
|
-
--toast-bg: var(--color-success);
|
|
42
|
-
--toast-border: var(--color-success);
|
|
43
|
-
--toast-color: var(--color-white);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
.toast-item.danger,
|
|
47
|
-
.toast-item.error {
|
|
48
|
-
--toast-bg: var(--color-danger);
|
|
49
|
-
--toast-border: var(--color-danger);
|
|
50
|
-
--toast-color: var(--color-white);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
.toast-item.warning {
|
|
54
|
-
--toast-bg: var(--color-warning);
|
|
55
|
-
--toast-border: var(--color-warning);
|
|
56
|
-
--toast-color: var(--color-white);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
.toast-item.info {
|
|
60
|
-
--toast-bg: var(--color-info);
|
|
61
|
-
--toast-border: var(--color-info);
|
|
62
|
-
--toast-color: var(--color-white);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
.toast-desc {
|
|
66
|
-
font-size: var(--text-sm);
|
|
67
|
-
margin-top: var(--spacing-gap-small);
|
|
68
|
-
opacity: 0.9;
|
|
69
|
-
}
|