atomsystem 0.1.25 → 0.1.27
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 +13 -5
- package/app/templates/Badge/Badge.tsx +2 -4
- package/app/templates/Button/Button.tsx +2 -24
- package/app/templates/Card/Card.tsx +4 -5
- 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 -88
- package/app/templates/Modal/Modal.tsx +7 -7
- package/app/templates/Navigation/Navigation.tsx +306 -0
- 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 +87 -80
- package/app/templates/Toast/Toast.tsx +4 -4
- package/build/build-tokens.js +204 -53
- package/build/css/_variables.css +315 -0
- package/build/typescript/theme.ts +14 -0
- package/cli/add.js +35 -3
- 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 +10 -6
- package/tokens/tokens.json +949 -0
- package/app/templates/GlobalNav/GlobalNav.tsx +0 -199
- package/token.json +0 -492
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import Image from "next/image";
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
import { usePathname } from "next/navigation";
|
|
6
|
+
import clsx from "clsx";
|
|
7
|
+
import { useMediaQuery } from "react-responsive";
|
|
8
|
+
|
|
9
|
+
interface NavItemsAttr {
|
|
10
|
+
type?: "topnav" | "sidenav";
|
|
11
|
+
navMenu: {
|
|
12
|
+
name: string;
|
|
13
|
+
path?: string;
|
|
14
|
+
}[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Custom CSS classes are now used via css/navigation.css
|
|
18
|
+
|
|
19
|
+
const handleNavToggle = (targetId: string) => {
|
|
20
|
+
const el = document.getElementById(targetId);
|
|
21
|
+
if (el) {
|
|
22
|
+
const isHidden = el.style.display === "none" || el.style.display === "";
|
|
23
|
+
el.style.display = isHidden ? "block" : "none";
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const Navigation = ({
|
|
28
|
+
children,
|
|
29
|
+
id,
|
|
30
|
+
className,
|
|
31
|
+
type = "topnav",
|
|
32
|
+
}: {
|
|
33
|
+
children: React.ReactNode;
|
|
34
|
+
id?: string;
|
|
35
|
+
className?: string;
|
|
36
|
+
type?: "topnav" | "sidenav";
|
|
37
|
+
}) => {
|
|
38
|
+
const isMobile = useMediaQuery({ maxWidth: 767 });
|
|
39
|
+
|
|
40
|
+
const mobileClose = (e: React.MouseEvent) => {
|
|
41
|
+
if (isMobile) {
|
|
42
|
+
if (id) {
|
|
43
|
+
handleNavToggle(id);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<nav
|
|
50
|
+
id={id ? id : undefined}
|
|
51
|
+
className={`nav-container nav-${type} ${className} overflow-auto`}
|
|
52
|
+
onClick={mobileClose}
|
|
53
|
+
>
|
|
54
|
+
{children}
|
|
55
|
+
</nav>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const NavItems = ({ navMenu, type = "sidenav" }: NavItemsAttr) => {
|
|
60
|
+
const pathname = usePathname();
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<ul className={clsx(`nav-${type}`, { "flex flex-row items-center nav-gap": type === "topnav" })}>
|
|
64
|
+
{navMenu.map((menu) => {
|
|
65
|
+
return (
|
|
66
|
+
<li className="relative group" key={menu.name}>
|
|
67
|
+
<div className={clsx("flex justify-between items-center pr-4")}>
|
|
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>
|
|
74
|
+
</div>
|
|
75
|
+
</li>
|
|
76
|
+
);
|
|
77
|
+
})}
|
|
78
|
+
</ul>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const NavToggleButton = ({
|
|
83
|
+
children,
|
|
84
|
+
targetId,
|
|
85
|
+
className,
|
|
86
|
+
}: {
|
|
87
|
+
children: React.ReactNode;
|
|
88
|
+
targetId: string;
|
|
89
|
+
className?: string;
|
|
90
|
+
}) => {
|
|
91
|
+
return (
|
|
92
|
+
<button className={className} onClick={() => handleNavToggle(targetId)}>
|
|
93
|
+
{children}
|
|
94
|
+
</button>
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const Sidebar = ({ navMenu, type = "sidenav" }: NavItemsAttr) => {
|
|
99
|
+
return (
|
|
100
|
+
<nav
|
|
101
|
+
className={clsx(
|
|
102
|
+
`relative nav-container nav-${type} w-full md:w-[320px] h-full z-20 left-0 overflow-auto`
|
|
103
|
+
)}
|
|
104
|
+
>
|
|
105
|
+
<ul>
|
|
106
|
+
{navMenu.map((menu, idx) => {
|
|
107
|
+
return (
|
|
108
|
+
<li className="relative group" key={menu.name + idx}>
|
|
109
|
+
<div className={clsx("flex justify-between items-center pr-4")}>
|
|
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>
|
|
118
|
+
</div>
|
|
119
|
+
</li>
|
|
120
|
+
);
|
|
121
|
+
})}
|
|
122
|
+
</ul>
|
|
123
|
+
</nav>
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const GlobalNav = ({
|
|
128
|
+
type = "sidebar",
|
|
129
|
+
navMenu,
|
|
130
|
+
brand = true,
|
|
131
|
+
}: {
|
|
132
|
+
type?: "sidebar" | "topmenu" | "mobile";
|
|
133
|
+
navMenu: {
|
|
134
|
+
name: string;
|
|
135
|
+
path?: string;
|
|
136
|
+
subItems?: { name: string; path: string }[];
|
|
137
|
+
label?: boolean;
|
|
138
|
+
}[];
|
|
139
|
+
brand?: boolean;
|
|
140
|
+
}) => {
|
|
141
|
+
const [windowSize, setWindowSize] = useState<number>(); // 초기값 설정
|
|
142
|
+
const [toggleMenu, setToggleMenu] = useState<boolean>(true);
|
|
143
|
+
const pathname = usePathname();
|
|
144
|
+
const [openSubMenu, setOpenSubMenu] = useState<Record<string, boolean>>({});
|
|
145
|
+
|
|
146
|
+
// 윈도우 사이즈 추적
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
const handleResize = () => {
|
|
149
|
+
setWindowSize(window.innerWidth); // 윈도우 사이즈 업데이트
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
window.addEventListener("resize", handleResize); // 리사이즈 이벤트 리스너 추가
|
|
153
|
+
|
|
154
|
+
return () => window.removeEventListener("resize", handleResize); // 컴포넌트 언마운트 시 리스너 제거
|
|
155
|
+
}, [windowSize]);
|
|
156
|
+
|
|
157
|
+
// 초기 토글 값 설정
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
if (window.innerWidth > 768) {
|
|
160
|
+
} else {
|
|
161
|
+
setToggleMenu(false);
|
|
162
|
+
}
|
|
163
|
+
}, []);
|
|
164
|
+
|
|
165
|
+
const toggleSubMenu = (menuName: string) => {
|
|
166
|
+
setOpenSubMenu((prev) => ({
|
|
167
|
+
...prev,
|
|
168
|
+
[menuName]: !prev[menuName],
|
|
169
|
+
}));
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const navType = type === "sidebar" ? "sidenav" : "topnav";
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<nav
|
|
176
|
+
className={clsx(
|
|
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" }
|
|
180
|
+
)}
|
|
181
|
+
>
|
|
182
|
+
{/* brand */}
|
|
183
|
+
{brand ? (
|
|
184
|
+
<div className="brand">
|
|
185
|
+
<div className={`flex items-center justify-between`}>
|
|
186
|
+
<Link href={"/"}>
|
|
187
|
+
<Image
|
|
188
|
+
src={"/images/global/logo.png"}
|
|
189
|
+
alt="Logo"
|
|
190
|
+
width={180}
|
|
191
|
+
height={45}
|
|
192
|
+
draggable="false"
|
|
193
|
+
></Image>
|
|
194
|
+
</Link>
|
|
195
|
+
<button
|
|
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`}
|
|
197
|
+
onClick={() => setToggleMenu(!toggleMenu)}
|
|
198
|
+
>
|
|
199
|
+
<span
|
|
200
|
+
className={`material-symbols-outlined nav-text-standard nav-${navType}`}
|
|
201
|
+
style={{ fontSize: "16px" }}
|
|
202
|
+
>
|
|
203
|
+
menu
|
|
204
|
+
</span>
|
|
205
|
+
</button>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
) : null}
|
|
209
|
+
{/* List */}
|
|
210
|
+
<ul
|
|
211
|
+
className={clsx("", {
|
|
212
|
+
"flex flex-row": type == "topmenu",
|
|
213
|
+
})}
|
|
214
|
+
>
|
|
215
|
+
{navMenu.map((menu, idx) => {
|
|
216
|
+
switch (true) {
|
|
217
|
+
case menu.label === true && type == "sidebar":
|
|
218
|
+
return (
|
|
219
|
+
<p
|
|
220
|
+
key={menu.name + idx}
|
|
221
|
+
className={`nav-text-sub nav-${navType} mt-2 mb-0 nav-padding`}
|
|
222
|
+
>
|
|
223
|
+
{menu.name}
|
|
224
|
+
</p>
|
|
225
|
+
);
|
|
226
|
+
default:
|
|
227
|
+
return (
|
|
228
|
+
<li className="relative group" key={menu.name}>
|
|
229
|
+
<div
|
|
230
|
+
className={clsx("flex justify-between items-center pr-4")}
|
|
231
|
+
>
|
|
232
|
+
<Link
|
|
233
|
+
href={menu.path || "#"}
|
|
234
|
+
className={clsx(
|
|
235
|
+
`block nav-text-standard nav-${navType} nav-padding`,
|
|
236
|
+
{
|
|
237
|
+
"nav-text-active":
|
|
238
|
+
(menu.path && menu.path == pathname) ||
|
|
239
|
+
menu.subItems?.some((sub) => sub.path === pathname),
|
|
240
|
+
}
|
|
241
|
+
)}
|
|
242
|
+
>
|
|
243
|
+
{menu.name}
|
|
244
|
+
</Link>
|
|
245
|
+
{menu.subItems && type == "sidebar" ? (
|
|
246
|
+
<button
|
|
247
|
+
className="cursor-pointer"
|
|
248
|
+
onClick={() => toggleSubMenu(menu.name)}
|
|
249
|
+
>
|
|
250
|
+
<span
|
|
251
|
+
className={clsx(
|
|
252
|
+
`material-symbols-outlined nav-text-sub nav-${navType} transition-transform`,
|
|
253
|
+
{
|
|
254
|
+
"rotate-90": openSubMenu[menu.name],
|
|
255
|
+
}
|
|
256
|
+
)}
|
|
257
|
+
style={{ fontSize: "16px" }}
|
|
258
|
+
>
|
|
259
|
+
keyboard_arrow_right
|
|
260
|
+
</span>
|
|
261
|
+
</button>
|
|
262
|
+
) : null}
|
|
263
|
+
</div>
|
|
264
|
+
{menu.subItems ? (
|
|
265
|
+
<ul
|
|
266
|
+
className={clsx(
|
|
267
|
+
"block",
|
|
268
|
+
{
|
|
269
|
+
[`hidden absolute top-[42px] group-hover:block hover:block nav-bg nav-${navType} border nav-border`]:
|
|
270
|
+
type == "topmenu",
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
hidden: !openSubMenu[menu.name] && type == "sidebar",
|
|
274
|
+
}
|
|
275
|
+
)}
|
|
276
|
+
>
|
|
277
|
+
{menu.subItems.map((submenu) => {
|
|
278
|
+
return (
|
|
279
|
+
<li key={submenu.name}>
|
|
280
|
+
<Link
|
|
281
|
+
className={clsx(
|
|
282
|
+
`block nav-text-standard nav-${navType} nav-padding`,
|
|
283
|
+
{
|
|
284
|
+
"nav-text-active": submenu.path == pathname,
|
|
285
|
+
},
|
|
286
|
+
{ "pl-12 ": type == "sidebar" }
|
|
287
|
+
)}
|
|
288
|
+
href={submenu.path}
|
|
289
|
+
>
|
|
290
|
+
{submenu.name}
|
|
291
|
+
</Link>
|
|
292
|
+
</li>
|
|
293
|
+
);
|
|
294
|
+
})}
|
|
295
|
+
</ul>
|
|
296
|
+
) : null}
|
|
297
|
+
</li>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
})}
|
|
301
|
+
</ul>
|
|
302
|
+
</nav>
|
|
303
|
+
);
|
|
304
|
+
};
|
|
305
|
+
export default Navigation;
|
|
306
|
+
export { Sidebar, GlobalNav, NavToggleButton, NavItems };
|
|
@@ -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,125 @@
|
|
|
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
|
-
|
|
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 = () => {
|
|
99
|
+
const [theme, setTheme] = useState<Record<string, string>>({});
|
|
97
100
|
const [isThemeLoading, setThemeLoading] = useState(false);
|
|
98
|
-
|
|
101
|
+
|
|
99
102
|
useEffect(() => {
|
|
100
|
-
// 로컬 스토리지에서 테마 가져오기
|
|
101
103
|
const listenStorageChange = () => {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
const newThemes: Record<string, string> = {};
|
|
105
|
+
Object.entries(themes).forEach(([group, groupThemes]) => {
|
|
106
|
+
const storedKey = group === "global" ? "theme" : `theme-${group}`;
|
|
107
|
+
const currentTheme = localStorage.getItem(storedKey) || groupThemes[0];
|
|
108
|
+
newThemes[group] = currentTheme;
|
|
109
|
+
|
|
110
|
+
document.documentElement.classList.remove(...groupThemes);
|
|
111
|
+
document.documentElement.classList.add(currentTheme);
|
|
112
|
+
});
|
|
113
|
+
setTheme(newThemes);
|
|
106
114
|
};
|
|
115
|
+
|
|
107
116
|
listenStorageChange();
|
|
108
117
|
window.addEventListener("storage", listenStorageChange);
|
|
109
118
|
setThemeLoading(true);
|
|
110
119
|
return () => window.removeEventListener("storage", listenStorageChange);
|
|
111
|
-
}, [
|
|
120
|
+
}, []);
|
|
112
121
|
|
|
113
|
-
if (isThemeLoading
|
|
122
|
+
if (isThemeLoading === false) {
|
|
114
123
|
return null;
|
|
115
124
|
}
|
|
116
|
-
|
|
117
|
-
return <>{children}</>;
|
|
118
125
|
};
|