atomsystem 0.1.26 → 0.1.28
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 +53 -5
- package/app/templates/Badge/Badge.tsx +2 -4
- package/app/templates/Button/Button.tsx +2 -24
- package/app/templates/Card/Card.tsx +4 -13
- package/app/templates/CodeBlock/CodeBlock.tsx +18 -2
- package/app/templates/Collapse/Collapse.tsx +10 -10
- package/app/templates/{Input/Input.tsx → Form/Form.tsx} +69 -90
- package/app/templates/Modal/Modal.tsx +7 -7
- package/app/templates/Navigation/Navigation.tsx +44 -52
- package/app/templates/Pagination/Pagination.tsx +6 -6
- package/app/templates/Tab/Tab.tsx +10 -10
- package/app/templates/Table/Table.tsx +9 -18
- package/app/templates/Theme/Theme.tsx +83 -80
- package/app/templates/Toast/Toast.tsx +4 -4
- package/build/build-tokens.js +204 -53
- package/build/css/_variables.css +269 -0
- package/build/typescript/theme.ts +14 -0
- package/cli/add.js +34 -2
- package/cli/init.js +82 -14
- package/css/badge.css +28 -0
- package/css/button.css +75 -0
- package/css/card.css +17 -0
- package/css/collapse.css +33 -0
- package/css/form.css +115 -0
- package/css/modal.css +98 -0
- package/css/navigation.css +69 -0
- package/css/pagination.css +66 -0
- package/css/tab.css +56 -0
- package/css/table.css +58 -0
- package/css/text.css +25 -0
- package/css/toast.css +69 -0
- package/package.json +9 -6
- package/tokens/tokens.json +949 -0
- package/app/templates/NavToggle/NavToggle.tsx +0 -1
- package/token.json +0 -492
|
@@ -7,16 +7,16 @@ import clsx from "clsx";
|
|
|
7
7
|
import { useMediaQuery } from "react-responsive";
|
|
8
8
|
|
|
9
9
|
interface NavItemsAttr {
|
|
10
|
+
type?: "topnav" | "sidenav";
|
|
10
11
|
navMenu: {
|
|
11
12
|
name: string;
|
|
12
13
|
path?: string;
|
|
13
14
|
}[];
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
"bg-[var(--background-card)] text-text-standard border-[var(--color-border)]";
|
|
17
|
+
// Custom CSS classes are now used via css/navigation.css
|
|
18
18
|
|
|
19
|
-
const
|
|
19
|
+
const handleNavToggle = (targetId: string) => {
|
|
20
20
|
const el = document.getElementById(targetId);
|
|
21
21
|
if (el) {
|
|
22
22
|
const isHidden = el.style.display === "none" || el.style.display === "";
|
|
@@ -28,18 +28,19 @@ const Navigation = ({
|
|
|
28
28
|
children,
|
|
29
29
|
id,
|
|
30
30
|
className,
|
|
31
|
+
type = "topnav",
|
|
31
32
|
}: {
|
|
32
33
|
children: React.ReactNode;
|
|
33
34
|
id?: string;
|
|
34
35
|
className?: string;
|
|
36
|
+
type?: "topnav" | "sidenav";
|
|
35
37
|
}) => {
|
|
36
38
|
const isMobile = useMediaQuery({ maxWidth: 767 });
|
|
37
39
|
|
|
38
|
-
const mobileClose = (
|
|
40
|
+
const mobileClose = () => {
|
|
39
41
|
if (isMobile) {
|
|
40
|
-
e.preventDefault();
|
|
41
42
|
if (id) {
|
|
42
|
-
|
|
43
|
+
handleNavToggle(id);
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
46
|
};
|
|
@@ -47,7 +48,7 @@ const Navigation = ({
|
|
|
47
48
|
return (
|
|
48
49
|
<nav
|
|
49
50
|
id={id ? id : undefined}
|
|
50
|
-
className={
|
|
51
|
+
className={`nav-container nav-${type} ${className} overflow-auto`}
|
|
51
52
|
onClick={mobileClose}
|
|
52
53
|
>
|
|
53
54
|
{children}
|
|
@@ -55,27 +56,21 @@ const Navigation = ({
|
|
|
55
56
|
);
|
|
56
57
|
};
|
|
57
58
|
|
|
58
|
-
const NavItems = ({ navMenu }: NavItemsAttr) => {
|
|
59
|
+
const NavItems = ({ navMenu, type = "sidenav" }: NavItemsAttr) => {
|
|
59
60
|
const pathname = usePathname();
|
|
60
61
|
|
|
61
62
|
return (
|
|
62
|
-
<ul>
|
|
63
|
+
<ul className={clsx(`nav-${type}`, { "flex flex-row items-center nav-gap": type === "topnav" })}>
|
|
63
64
|
{navMenu.map((menu) => {
|
|
64
65
|
return (
|
|
65
66
|
<li className="relative group" key={menu.name}>
|
|
66
67
|
<div className={clsx("flex justify-between items-center pr-4")}>
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
</Link>
|
|
74
|
-
) : (
|
|
75
|
-
<p className={`px-4 py-2 block text-[var(--text-standard)]`}>
|
|
76
|
-
{menu.name}
|
|
77
|
-
</p>
|
|
78
|
-
)}
|
|
68
|
+
<Link
|
|
69
|
+
href={menu.path || "#"}
|
|
70
|
+
className={`${pathname === menu.path ? "nav-text-active" : "nav-text-standard"} nav-${type} block hover:opacity-80 transition-opacity nav-padding`}
|
|
71
|
+
>
|
|
72
|
+
{menu.name}
|
|
73
|
+
</Link>
|
|
79
74
|
</div>
|
|
80
75
|
</li>
|
|
81
76
|
);
|
|
@@ -94,36 +89,32 @@ const NavToggleButton = ({
|
|
|
94
89
|
className?: string;
|
|
95
90
|
}) => {
|
|
96
91
|
return (
|
|
97
|
-
<button className={className} onClick={() =>
|
|
92
|
+
<button className={className} onClick={() => handleNavToggle(targetId)}>
|
|
98
93
|
{children}
|
|
99
94
|
</button>
|
|
100
95
|
);
|
|
101
96
|
};
|
|
102
97
|
|
|
103
|
-
const Sidebar = ({ navMenu }: NavItemsAttr) => {
|
|
98
|
+
const Sidebar = ({ navMenu, type = "sidenav" }: NavItemsAttr) => {
|
|
104
99
|
return (
|
|
105
100
|
<nav
|
|
106
101
|
className={clsx(
|
|
107
|
-
`relative
|
|
102
|
+
`relative nav-container nav-${type} w-full md:w-[320px] h-full z-20 left-0 overflow-auto`
|
|
108
103
|
)}
|
|
109
104
|
>
|
|
110
105
|
<ul>
|
|
111
106
|
{navMenu.map((menu, idx) => {
|
|
112
107
|
return (
|
|
113
|
-
<li className="relative group" key={menu.name}>
|
|
108
|
+
<li className="relative group" key={menu.name + idx}>
|
|
114
109
|
<div className={clsx("flex justify-between items-center pr-4")}>
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
</Link>
|
|
124
|
-
) : (
|
|
125
|
-
<p></p>
|
|
126
|
-
)}
|
|
110
|
+
<Link
|
|
111
|
+
href={menu.path || "#"}
|
|
112
|
+
className={clsx(
|
|
113
|
+
`block nav-text-standard nav-${type} hover:opacity-80 transition-opacity nav-padding`
|
|
114
|
+
)}
|
|
115
|
+
>
|
|
116
|
+
{menu.name}
|
|
117
|
+
</Link>
|
|
127
118
|
</div>
|
|
128
119
|
</li>
|
|
129
120
|
);
|
|
@@ -178,12 +169,14 @@ const GlobalNav = ({
|
|
|
178
169
|
}));
|
|
179
170
|
};
|
|
180
171
|
|
|
172
|
+
const navType = type === "sidebar" ? "sidenav" : "topnav";
|
|
173
|
+
|
|
181
174
|
return (
|
|
182
175
|
<nav
|
|
183
176
|
className={clsx(
|
|
184
|
-
`fixed top-0 md:relative w-full
|
|
185
|
-
{
|
|
186
|
-
{
|
|
177
|
+
`fixed top-0 md:relative w-full nav-container nav-${navType} z-20 left-0 ${toggleMenu ? "block" : "hidden"}`,
|
|
178
|
+
{ [`h-full max-w-80 overflow-auto border-r nav-border`]: type == "sidebar" },
|
|
179
|
+
{ [`h-auto flex flex-row items-center border-b nav-border nav-${navType}`]: type == "topmenu" }
|
|
187
180
|
)}
|
|
188
181
|
>
|
|
189
182
|
{/* brand */}
|
|
@@ -200,11 +193,11 @@ const GlobalNav = ({
|
|
|
200
193
|
></Image>
|
|
201
194
|
</Link>
|
|
202
195
|
<button
|
|
203
|
-
className={`fixed shadow-md rounded-full bg-
|
|
196
|
+
className={`fixed shadow-md rounded-full nav-bg nav-${navType} border nav-border w-12 h-12 top-6 left-6 cursor-pointer z-50`}
|
|
204
197
|
onClick={() => setToggleMenu(!toggleMenu)}
|
|
205
198
|
>
|
|
206
199
|
<span
|
|
207
|
-
className=
|
|
200
|
+
className={`material-symbols-outlined nav-text-standard nav-${navType}`}
|
|
208
201
|
style={{ fontSize: "16px" }}
|
|
209
202
|
>
|
|
210
203
|
menu
|
|
@@ -225,25 +218,24 @@ const GlobalNav = ({
|
|
|
225
218
|
return (
|
|
226
219
|
<p
|
|
227
220
|
key={menu.name + idx}
|
|
228
|
-
className=
|
|
221
|
+
className={`nav-text-sub nav-${navType} mt-2 mb-0 nav-padding`}
|
|
229
222
|
>
|
|
230
223
|
{menu.name}
|
|
231
224
|
</p>
|
|
232
225
|
);
|
|
233
226
|
default:
|
|
234
|
-
if (!menu.path) return null;
|
|
235
227
|
return (
|
|
236
228
|
<li className="relative group" key={menu.name}>
|
|
237
229
|
<div
|
|
238
230
|
className={clsx("flex justify-between items-center pr-4")}
|
|
239
231
|
>
|
|
240
232
|
<Link
|
|
241
|
-
href={menu.path}
|
|
233
|
+
href={menu.path || "#"}
|
|
242
234
|
className={clsx(
|
|
243
|
-
|
|
235
|
+
`block nav-text-standard nav-${navType} nav-padding`,
|
|
244
236
|
{
|
|
245
|
-
"text-
|
|
246
|
-
menu.path == pathname ||
|
|
237
|
+
"nav-text-active":
|
|
238
|
+
(menu.path && menu.path == pathname) ||
|
|
247
239
|
menu.subItems?.some((sub) => sub.path === pathname),
|
|
248
240
|
}
|
|
249
241
|
)}
|
|
@@ -257,7 +249,7 @@ const GlobalNav = ({
|
|
|
257
249
|
>
|
|
258
250
|
<span
|
|
259
251
|
className={clsx(
|
|
260
|
-
|
|
252
|
+
`material-symbols-outlined nav-text-sub nav-${navType} transition-transform`,
|
|
261
253
|
{
|
|
262
254
|
"rotate-90": openSubMenu[menu.name],
|
|
263
255
|
}
|
|
@@ -274,7 +266,7 @@ const GlobalNav = ({
|
|
|
274
266
|
className={clsx(
|
|
275
267
|
"block",
|
|
276
268
|
{
|
|
277
|
-
|
|
269
|
+
[`hidden absolute top-[42px] group-hover:block hover:block nav-bg nav-${navType} border nav-border`]:
|
|
278
270
|
type == "topmenu",
|
|
279
271
|
},
|
|
280
272
|
{
|
|
@@ -287,9 +279,9 @@ const GlobalNav = ({
|
|
|
287
279
|
<li key={submenu.name}>
|
|
288
280
|
<Link
|
|
289
281
|
className={clsx(
|
|
290
|
-
|
|
282
|
+
`block nav-text-standard nav-${navType} nav-padding`,
|
|
291
283
|
{
|
|
292
|
-
"text-
|
|
284
|
+
"nav-text-active": submenu.path == pathname,
|
|
293
285
|
},
|
|
294
286
|
{ "pl-12 ": type == "sidebar" }
|
|
295
287
|
)}
|
|
@@ -34,7 +34,7 @@ const Pagination = ({
|
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
return (
|
|
37
|
-
<div className="
|
|
37
|
+
<div className="pagination">
|
|
38
38
|
<button
|
|
39
39
|
disabled={pageStart === 1}
|
|
40
40
|
onClick={() => {
|
|
@@ -43,11 +43,11 @@ const Pagination = ({
|
|
|
43
43
|
setPageStart(newStart);
|
|
44
44
|
onPageChange(newStart);
|
|
45
45
|
}}
|
|
46
|
-
className="
|
|
46
|
+
className="pagination-btn"
|
|
47
47
|
>
|
|
48
48
|
<span className="material-symbols-outlined">keyboard_arrow_left</span>
|
|
49
49
|
</button>
|
|
50
|
-
<ul className="
|
|
50
|
+
<ul className="pagination-list">
|
|
51
51
|
{[...Array(pageRange)].map((_, i) => {
|
|
52
52
|
const page = pageStart + i;
|
|
53
53
|
|
|
@@ -74,8 +74,8 @@ const Pagination = ({
|
|
|
74
74
|
}
|
|
75
75
|
scrollTop();
|
|
76
76
|
}}
|
|
77
|
-
className={`
|
|
78
|
-
currentPage === page ? "
|
|
77
|
+
className={`pagination-item ${
|
|
78
|
+
currentPage === page ? "pagination-item-active" : ""
|
|
79
79
|
}`}
|
|
80
80
|
>
|
|
81
81
|
{page}
|
|
@@ -93,7 +93,7 @@ const Pagination = ({
|
|
|
93
93
|
onPageChange(newStart);
|
|
94
94
|
}
|
|
95
95
|
}}
|
|
96
|
-
className="
|
|
96
|
+
className="pagination-btn"
|
|
97
97
|
>
|
|
98
98
|
<span className="material-symbols-outlined">keyboard_arrow_right</span>
|
|
99
99
|
</button>
|
|
@@ -6,12 +6,12 @@ import { useGSAP } from "@gsap/react";
|
|
|
6
6
|
|
|
7
7
|
const Tab = ({
|
|
8
8
|
tabs,
|
|
9
|
-
tabClass = "
|
|
10
|
-
tabNavItemClass = "
|
|
11
|
-
tabNavItemActiveClass = "
|
|
9
|
+
tabClass = "tab",
|
|
10
|
+
tabNavItemClass = "tab-nav-item",
|
|
11
|
+
tabNavItemActiveClass = "tab-nav-item-active",
|
|
12
12
|
tabContents,
|
|
13
|
-
tabContentClass = "
|
|
14
|
-
tabContentItem = "
|
|
13
|
+
tabContentClass = "tab-content",
|
|
14
|
+
tabContentItem = "tab-content-item",
|
|
15
15
|
expectionValue,
|
|
16
16
|
expectionLayout,
|
|
17
17
|
}: {
|
|
@@ -76,7 +76,7 @@ const Tab = ({
|
|
|
76
76
|
}, [activeContent]);
|
|
77
77
|
|
|
78
78
|
return (
|
|
79
|
-
<div className="flex flex-col">
|
|
79
|
+
<div className="flex flex-col w-full">
|
|
80
80
|
<div className={tabClass}>
|
|
81
81
|
{tabs.map((tab) => {
|
|
82
82
|
return (
|
|
@@ -84,9 +84,9 @@ const Tab = ({
|
|
|
84
84
|
key={tab.id}
|
|
85
85
|
id={tab.id}
|
|
86
86
|
onClick={() => handleTabActive(tab.target, tab.id)}
|
|
87
|
-
className={
|
|
87
|
+
className={`${tabNavItemClass} ${
|
|
88
88
|
activeTab == tab.id
|
|
89
|
-
?
|
|
89
|
+
? tabNavItemActiveClass
|
|
90
90
|
: ""
|
|
91
91
|
}`}
|
|
92
92
|
>
|
|
@@ -101,8 +101,8 @@ const Tab = ({
|
|
|
101
101
|
<div
|
|
102
102
|
key={tabContent.id}
|
|
103
103
|
id={tabContent.tag}
|
|
104
|
-
className={
|
|
105
|
-
activeTab == expectionValue ? expectionLayout :
|
|
104
|
+
className={`${tabContentItem} ${
|
|
105
|
+
activeTab == expectionValue ? expectionLayout : ""
|
|
106
106
|
}`}
|
|
107
107
|
>
|
|
108
108
|
{tabContent.content}
|
|
@@ -6,8 +6,8 @@ const Table = ({ tableData }: { tableData: Record<string, any>[] }) => {
|
|
|
6
6
|
// tableData가 제공되지 않았거나 비어있는 경우, 데이터 없음 메시지를 표시합니다.
|
|
7
7
|
if (!tableData || tableData.length === 0) {
|
|
8
8
|
return (
|
|
9
|
-
<div className="
|
|
10
|
-
<p
|
|
9
|
+
<div className="table-empty">
|
|
10
|
+
<p>표시할 데이터가 없습니다.</p>
|
|
11
11
|
</div>
|
|
12
12
|
);
|
|
13
13
|
}
|
|
@@ -15,17 +15,14 @@ const Table = ({ tableData }: { tableData: Record<string, any>[] }) => {
|
|
|
15
15
|
const headers = Object.keys(tableData[0]);
|
|
16
16
|
|
|
17
17
|
return (
|
|
18
|
-
<div className="
|
|
19
|
-
<table className="
|
|
20
|
-
<thead className="
|
|
18
|
+
<div className="table-container">
|
|
19
|
+
<table className="table">
|
|
20
|
+
<thead className="table-header">
|
|
21
21
|
<tr>
|
|
22
22
|
{/* 헤더 배열을 순회하며 각 헤더에 대한 <th>태그를 생성합니다. */}
|
|
23
23
|
{headers.map((header, index) => {
|
|
24
24
|
return (
|
|
25
|
-
<th
|
|
26
|
-
key={index}
|
|
27
|
-
className="px-6 py-3 text-center text-[var(--table-header-text)] tracking-wider border-b border-[var(--color-border)]"
|
|
28
|
-
>
|
|
25
|
+
<th key={index}>
|
|
29
26
|
{header}
|
|
30
27
|
</th>
|
|
31
28
|
);
|
|
@@ -33,20 +30,14 @@ const Table = ({ tableData }: { tableData: Record<string, any>[] }) => {
|
|
|
33
30
|
</tr>
|
|
34
31
|
</thead>
|
|
35
32
|
{/* 테이블 바디 부분 */}
|
|
36
|
-
<tbody className=
|
|
33
|
+
<tbody className="table-body">
|
|
37
34
|
{/* tableData 배열을 순회하며 각 데이터 객체(행)에 대한 <tr> 태그를 생성합니다. */}
|
|
38
35
|
{tableData.map((row, rowIndex) => {
|
|
39
36
|
return (
|
|
40
|
-
<tr
|
|
41
|
-
key={rowIndex}
|
|
42
|
-
className={` ${rowIndex % 2 === 0 ? `bg-[var(--table-body-background)]` : `bg-[var(--table-body-background-even)]`}`}
|
|
43
|
-
>
|
|
37
|
+
<tr key={rowIndex} className="table-row">
|
|
44
38
|
{/* 각 행 내에서 헤더 배열을 다시 순회하며 해당 헤더에 맞는 셀(<td>)을 생성합니다. */}
|
|
45
39
|
{headers.map((header, colIndex) => (
|
|
46
|
-
<td
|
|
47
|
-
key={colIndex} // 고유한 키를 제공합니다.
|
|
48
|
-
className={`px-4 py-2 text-[var(--text-body-text)] border-b border-[var(--color-border)]`}
|
|
49
|
-
>
|
|
40
|
+
<td key={colIndex} className="table-cell">
|
|
50
41
|
{/* 현재 행 객체(row)에서 해당 헤더(키)에 해당하는 값을 가져와 셀에 표시합니다. */}
|
|
51
42
|
{row[header]}
|
|
52
43
|
</td>
|
|
@@ -1,118 +1,121 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
|
-
import { Radio } from "../
|
|
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";
|
|
5
8
|
|
|
6
|
-
export const ThemeSelector = ({
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
type: "all" | "theme" | "input";
|
|
10
|
-
}) => {
|
|
11
|
-
const themes = ["light", "dark"];
|
|
12
|
-
const [selectedTheme, setSelectedTheme] = useState<string | null>("light");
|
|
13
|
-
// const inputThemes = ["inputStandard", "inputCircle"];
|
|
14
|
-
// const [selectedInputTheme, setSelectedInputTheme] = useState<string | null>(
|
|
15
|
-
// null
|
|
16
|
-
// );
|
|
9
|
+
export const ThemeSelector = () => {
|
|
10
|
+
const [selectedThemes, setSelectedThemes] = useState<Record<string, string>>({});
|
|
11
|
+
const [tempThemes, setTempThemes] = useState<Record<string, string>>({});
|
|
17
12
|
const [isThemeLoading, setThemeLoading] = useState(false);
|
|
13
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
18
14
|
|
|
19
15
|
useEffect(() => {
|
|
20
16
|
const chageThemeValue = () => {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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);
|
|
27
25
|
};
|
|
28
26
|
|
|
29
27
|
chageThemeValue();
|
|
30
28
|
setThemeLoading(true);
|
|
31
29
|
}, []);
|
|
32
30
|
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
const handleTempThemeChange = (group: string, theme: string) => {
|
|
32
|
+
setTempThemes((prev) => ({ ...prev, [group]: theme }));
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const handleApply = () => {
|
|
35
36
|
if (typeof window !== "undefined") {
|
|
36
|
-
|
|
37
|
+
Object.entries(tempThemes).forEach(([group, theme]) => {
|
|
38
|
+
const storedKey = group === "global" ? "theme" : `theme-${group}`;
|
|
39
|
+
localStorage.setItem(storedKey, theme);
|
|
40
|
+
});
|
|
37
41
|
window.dispatchEvent(new Event("storage"));
|
|
38
|
-
|
|
42
|
+
setSelectedThemes(tempThemes);
|
|
43
|
+
setIsModalOpen(false);
|
|
39
44
|
}
|
|
40
45
|
};
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// };
|
|
49
|
-
if (isThemeLoading == false) {
|
|
46
|
+
|
|
47
|
+
const handleOpenModal = () => {
|
|
48
|
+
setTempThemes(selectedThemes); // Reset to current applied themes
|
|
49
|
+
setIsModalOpen(true);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (isThemeLoading === false) {
|
|
50
53
|
return null;
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
return (
|
|
54
|
-
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
))}
|
|
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>
|
|
68
72
|
</div>
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
+
</>
|
|
85
95
|
);
|
|
86
96
|
};
|
|
87
97
|
|
|
88
|
-
export const
|
|
89
|
-
children,
|
|
90
|
-
className,
|
|
91
|
-
}: {
|
|
92
|
-
children: React.ReactNode;
|
|
93
|
-
className: string;
|
|
94
|
-
}) => {
|
|
95
|
-
const [theme, setTheme] = useState<string | null>(null); // 테마 상태 추가
|
|
96
|
-
const [inputTheme, setInputTheme] = useState<string | null>(null); // 테마 상태 추가
|
|
98
|
+
export const ThemeProvider = () => {
|
|
97
99
|
const [isThemeLoading, setThemeLoading] = useState(false);
|
|
98
|
-
|
|
100
|
+
|
|
99
101
|
useEffect(() => {
|
|
100
|
-
// 로컬 스토리지에서 테마 가져오기
|
|
101
102
|
const listenStorageChange = () => {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
+
});
|
|
106
110
|
};
|
|
111
|
+
|
|
107
112
|
listenStorageChange();
|
|
108
113
|
window.addEventListener("storage", listenStorageChange);
|
|
109
114
|
setThemeLoading(true);
|
|
110
115
|
return () => window.removeEventListener("storage", listenStorageChange);
|
|
111
|
-
}, [
|
|
116
|
+
}, []);
|
|
112
117
|
|
|
113
|
-
if (isThemeLoading
|
|
118
|
+
if (isThemeLoading === false) {
|
|
114
119
|
return null;
|
|
115
120
|
}
|
|
116
|
-
|
|
117
|
-
return <>{children}</>;
|
|
118
121
|
};
|
|
@@ -9,7 +9,7 @@ gsap.registerPlugin(useGSAP);
|
|
|
9
9
|
// ✅ Toast Message 타입 정의
|
|
10
10
|
export type ToastMessage = {
|
|
11
11
|
id: string;
|
|
12
|
-
status
|
|
12
|
+
status?: "standard" | "primary" | "secondary" | "success" | "danger" | "warning" | "info" | "error";
|
|
13
13
|
message: string;
|
|
14
14
|
desc?: string;
|
|
15
15
|
};
|
|
@@ -55,7 +55,7 @@ export const Toast = () => {
|
|
|
55
55
|
}, []);
|
|
56
56
|
|
|
57
57
|
return (
|
|
58
|
-
<div className="
|
|
58
|
+
<div className="toast-container">
|
|
59
59
|
{messages.map((message) => (
|
|
60
60
|
<ToastItem
|
|
61
61
|
key={message.id}
|
|
@@ -97,10 +97,10 @@ const ToastItem = ({
|
|
|
97
97
|
return (
|
|
98
98
|
<div
|
|
99
99
|
ref={elRef}
|
|
100
|
-
className=
|
|
100
|
+
className={`toast-item ${message.status || "standard"}`}
|
|
101
101
|
>
|
|
102
102
|
<p>{message.message}</p>
|
|
103
|
-
{message.desc ? <p className="
|
|
103
|
+
{message.desc ? <p className="toast-desc">{message.desc}</p> : null}
|
|
104
104
|
</div>
|
|
105
105
|
);
|
|
106
106
|
};
|