atomsystem 0.1.19 → 0.1.20
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/app/templates/Animations/Animation.tsx +282 -0
- package/app/templates/Button/Button.tsx +3 -0
- package/app/templates/Card/Card.tsx +1 -1
- package/app/templates/GlobalNav/GlobalNav.tsx +113 -41
- package/app/templates/Pagination/Pagination.tsx +104 -0
- package/app/templates/Tab/Tab.tsx +31 -7
- package/app/templates/Table/Table.tsx +63 -0
- package/app/templates/Toast/Toast.tsx +1 -1
- package/cli/add.js +4 -0
- package/package.json +59 -59
- package/token.json +36 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { ReactNode, useEffect, useRef, useState } from "react";
|
|
3
|
+
import gsap from "gsap";
|
|
4
|
+
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
|
5
|
+
import { ScrollToPlugin } from "gsap/ScrollToPlugin";
|
|
6
|
+
import { useGSAP } from "@gsap/react";
|
|
7
|
+
|
|
8
|
+
gsap.registerPlugin(ScrollTrigger, ScrollToPlugin, useGSAP);
|
|
9
|
+
|
|
10
|
+
export const UpAnimation = ({
|
|
11
|
+
children,
|
|
12
|
+
trigger,
|
|
13
|
+
toggleActions = "play none reverse none",
|
|
14
|
+
overflow = "overflow-hidden",
|
|
15
|
+
stagger,
|
|
16
|
+
className,
|
|
17
|
+
markers,
|
|
18
|
+
AnimationContainer,
|
|
19
|
+
}: {
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
trigger?: string | HTMLElement;
|
|
22
|
+
toggleActions?: string;
|
|
23
|
+
overflow?: string;
|
|
24
|
+
className?: string;
|
|
25
|
+
stagger?: boolean;
|
|
26
|
+
markers?: boolean;
|
|
27
|
+
AnimationContainer?: gsap.core.Animation | undefined;
|
|
28
|
+
}) => {
|
|
29
|
+
const animationRef = useRef<HTMLDivElement>(null);
|
|
30
|
+
|
|
31
|
+
useGSAP(
|
|
32
|
+
() => {
|
|
33
|
+
if (!animationRef.current) return;
|
|
34
|
+
|
|
35
|
+
const target = stagger
|
|
36
|
+
? animationRef.current.children
|
|
37
|
+
: animationRef.current;
|
|
38
|
+
|
|
39
|
+
gsap.set(target, { y: 500, opacity: 0 }); // 초기 상태 설정
|
|
40
|
+
|
|
41
|
+
gsap.fromTo(
|
|
42
|
+
target,
|
|
43
|
+
{
|
|
44
|
+
y: 500,
|
|
45
|
+
opacity: 0,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
y: 0,
|
|
49
|
+
opacity: 1,
|
|
50
|
+
stagger: 0.2,
|
|
51
|
+
duration: 3,
|
|
52
|
+
ease: "power4.out",
|
|
53
|
+
scrollTrigger: {
|
|
54
|
+
trigger: trigger ? trigger : animationRef.current,
|
|
55
|
+
start: "top center",
|
|
56
|
+
end: "bottom bottom",
|
|
57
|
+
toggleActions: toggleActions,
|
|
58
|
+
markers: markers,
|
|
59
|
+
invalidateOnRefresh: true,
|
|
60
|
+
containerAnimation: AnimationContainer,
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
dependencies: [
|
|
67
|
+
trigger,
|
|
68
|
+
toggleActions,
|
|
69
|
+
markers,
|
|
70
|
+
stagger,
|
|
71
|
+
AnimationContainer,
|
|
72
|
+
],
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
return stagger ? (
|
|
77
|
+
<div ref={animationRef} className={`${overflow} ${className}`}>
|
|
78
|
+
{children}
|
|
79
|
+
</div>
|
|
80
|
+
) : (
|
|
81
|
+
<div className={`${overflow} ${className}`}>
|
|
82
|
+
<div ref={animationRef}>{children}</div>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const MaskAnimation = ({
|
|
88
|
+
children,
|
|
89
|
+
trigger,
|
|
90
|
+
maskColor,
|
|
91
|
+
markers,
|
|
92
|
+
}: {
|
|
93
|
+
children: ReactNode;
|
|
94
|
+
maskColor: string;
|
|
95
|
+
trigger?: string;
|
|
96
|
+
markers?: boolean;
|
|
97
|
+
}) => {
|
|
98
|
+
const animationRef = useRef<HTMLDivElement>(null);
|
|
99
|
+
const [maskSize, setMaskSize] = useState<number>(0);
|
|
100
|
+
|
|
101
|
+
// 리사이즈 핸들러
|
|
102
|
+
const updateMaskSize = () => {
|
|
103
|
+
if (animationRef.current) {
|
|
104
|
+
setMaskSize(animationRef.current.offsetWidth);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
updateMaskSize(); // 초기 사이즈 설정
|
|
110
|
+
|
|
111
|
+
window.addEventListener("resize", updateMaskSize);
|
|
112
|
+
return () => {
|
|
113
|
+
window.removeEventListener("resize", updateMaskSize);
|
|
114
|
+
};
|
|
115
|
+
}, []);
|
|
116
|
+
|
|
117
|
+
useGSAP(
|
|
118
|
+
() => {
|
|
119
|
+
if (!animationRef.current) return;
|
|
120
|
+
|
|
121
|
+
setMaskSize(animationRef.current.offsetWidth);
|
|
122
|
+
|
|
123
|
+
gsap.fromTo(
|
|
124
|
+
animationRef.current,
|
|
125
|
+
{
|
|
126
|
+
left: 0,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
left: -maskSize,
|
|
130
|
+
stagger: 0.2,
|
|
131
|
+
duration: 3,
|
|
132
|
+
ease: "power4.out",
|
|
133
|
+
scrollTrigger: {
|
|
134
|
+
trigger: trigger ? trigger : animationRef.current,
|
|
135
|
+
start: "top center",
|
|
136
|
+
end: "bottom bottom",
|
|
137
|
+
toggleActions: "play none none none",
|
|
138
|
+
markers: markers,
|
|
139
|
+
invalidateOnRefresh: true,
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
},
|
|
144
|
+
{ dependencies: [maskSize, trigger] }
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div className="relative overflow-hidden">
|
|
149
|
+
<div>{children}</div>
|
|
150
|
+
<div
|
|
151
|
+
ref={animationRef}
|
|
152
|
+
className={`absolute top-0 w-full h-full ${maskColor} z-20`}
|
|
153
|
+
></div>
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export const SlidAnimation = ({ children }: { children: React.ReactNode }) => {
|
|
159
|
+
const slideRef = useRef<HTMLDivElement>(null);
|
|
160
|
+
const [slideX, setSlideX] = useState(0);
|
|
161
|
+
|
|
162
|
+
useGSAP(
|
|
163
|
+
() => {
|
|
164
|
+
if (!slideRef.current) return;
|
|
165
|
+
|
|
166
|
+
if (!slideRef.current) return;
|
|
167
|
+
setSlideX(slideRef.current.offsetWidth / 2);
|
|
168
|
+
|
|
169
|
+
gsap.to(slideRef.current, {
|
|
170
|
+
x: -slideX,
|
|
171
|
+
duration: 40,
|
|
172
|
+
repeat: -1,
|
|
173
|
+
ease: "none",
|
|
174
|
+
});
|
|
175
|
+
},
|
|
176
|
+
{ dependencies: [slideX], scope: slideRef }
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<div ref={slideRef} className="flex gap-0 flex-nowrap">
|
|
181
|
+
{children}
|
|
182
|
+
{children}
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
export const HorizonScroll = ({
|
|
188
|
+
children,
|
|
189
|
+
callbackTween,
|
|
190
|
+
}: {
|
|
191
|
+
children: React.ReactNode;
|
|
192
|
+
callbackTween?: (tween: gsap.core.Tween) => void;
|
|
193
|
+
}) => {
|
|
194
|
+
const horizonRef = useRef<HTMLDivElement>(null);
|
|
195
|
+
const horizonWrapperRef = useRef<HTMLDivElement>(null);
|
|
196
|
+
|
|
197
|
+
useGSAP(() => {
|
|
198
|
+
if (!horizonWrapperRef.current) return;
|
|
199
|
+
if (!horizonRef.current) return;
|
|
200
|
+
|
|
201
|
+
const isLarge = window.matchMedia("(min-width: 640px)").matches;
|
|
202
|
+
if (!isLarge) return; // lg 미만이면 애니메이션 적용 안 함
|
|
203
|
+
|
|
204
|
+
const horizonWrapper = horizonWrapperRef.current;
|
|
205
|
+
const horizon = horizonRef.current;
|
|
206
|
+
|
|
207
|
+
const horizonTween = gsap.to(horizon, {
|
|
208
|
+
x: () => -1 * (horizonWrapper.scrollWidth - innerWidth),
|
|
209
|
+
ease: "none",
|
|
210
|
+
scrollTrigger: {
|
|
211
|
+
trigger: horizonWrapperRef.current,
|
|
212
|
+
pin: true,
|
|
213
|
+
start: "top top",
|
|
214
|
+
scrub: 1,
|
|
215
|
+
end: () => "+=" + (horizon.scrollWidth - innerWidth),
|
|
216
|
+
invalidateOnRefresh: true,
|
|
217
|
+
// markers: true,
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
if (callbackTween && isLarge && horizonTween) {
|
|
222
|
+
callbackTween(horizonTween);
|
|
223
|
+
}
|
|
224
|
+
}, [callbackTween]);
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<div
|
|
228
|
+
ref={horizonWrapperRef}
|
|
229
|
+
className="w-full h-full relative overflow-hidden"
|
|
230
|
+
>
|
|
231
|
+
<div
|
|
232
|
+
className="flex flex-col sm:flex-row flex-nowrap translate-x-0 w-full"
|
|
233
|
+
ref={horizonRef}
|
|
234
|
+
>
|
|
235
|
+
{children}
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
export const HorizonScrollNav = ({
|
|
242
|
+
sections,
|
|
243
|
+
listClass,
|
|
244
|
+
}: {
|
|
245
|
+
sections: { id: string; title: string }[];
|
|
246
|
+
listClass?: string;
|
|
247
|
+
}) => {
|
|
248
|
+
const scrollToSection = (id: string) => {
|
|
249
|
+
const el = document.getElementById(id);
|
|
250
|
+
if (!el) return;
|
|
251
|
+
|
|
252
|
+
const index = sections.findIndex((section) => section.id === id);
|
|
253
|
+
if (index === -1) return;
|
|
254
|
+
|
|
255
|
+
const isLarge = window.matchMedia("(min-width: 640px)").matches;
|
|
256
|
+
const scrollPos = isLarge ? el.offsetLeft : el.offsetTop;
|
|
257
|
+
|
|
258
|
+
gsap.to(window, {
|
|
259
|
+
scrollTo: {
|
|
260
|
+
y: scrollPos,
|
|
261
|
+
autoKill: false,
|
|
262
|
+
},
|
|
263
|
+
duration: 1,
|
|
264
|
+
});
|
|
265
|
+
};
|
|
266
|
+
return (
|
|
267
|
+
<ul className={`${listClass}`}>
|
|
268
|
+
{sections.map((section, index) => {
|
|
269
|
+
return (
|
|
270
|
+
<li key={index}>
|
|
271
|
+
<button
|
|
272
|
+
className="cursor-pointer text-sm"
|
|
273
|
+
onClick={() => scrollToSection(section.id)}
|
|
274
|
+
>
|
|
275
|
+
{section.title}
|
|
276
|
+
</button>
|
|
277
|
+
</li>
|
|
278
|
+
);
|
|
279
|
+
})}
|
|
280
|
+
</ul>
|
|
281
|
+
);
|
|
282
|
+
};
|
|
@@ -6,11 +6,13 @@ export const Button = ({
|
|
|
6
6
|
type,
|
|
7
7
|
style,
|
|
8
8
|
disabled,
|
|
9
|
+
onClick,
|
|
9
10
|
}: {
|
|
10
11
|
children: React.ReactNode;
|
|
11
12
|
type: "primary" | "secondary" | "success" | "danger" | "gray";
|
|
12
13
|
style?: React.CSSProperties;
|
|
13
14
|
disabled?: boolean;
|
|
15
|
+
onClick?: () => void;
|
|
14
16
|
}) => {
|
|
15
17
|
return (
|
|
16
18
|
<button
|
|
@@ -39,6 +41,7 @@ export const Button = ({
|
|
|
39
41
|
)}
|
|
40
42
|
style={style}
|
|
41
43
|
disabled={disabled}
|
|
44
|
+
onClick={onClick}
|
|
42
45
|
>
|
|
43
46
|
{children}
|
|
44
47
|
</button>
|
|
@@ -19,7 +19,7 @@ export const Card = ({
|
|
|
19
19
|
);
|
|
20
20
|
return (
|
|
21
21
|
<div
|
|
22
|
-
className={`${type == "standard" ? "bg-[var(--background-card)] border-[var(--color-border)]" : type == "group" ? "bg-
|
|
22
|
+
className={`${type == "standard" ? "bg-[var(--background-card)] border-[var(--color-border)]" : type == "group" ? "bg-[var(--background-wrapper)] border-none" : ""} ${!hasCardContent ? "p-4" : ""} rounded border ${className} overflow-auto`}
|
|
23
23
|
>
|
|
24
24
|
{children}
|
|
25
25
|
</div>
|
|
@@ -5,21 +5,21 @@ import { useEffect, useState } from "react";
|
|
|
5
5
|
import { usePathname } from "next/navigation";
|
|
6
6
|
import clsx from "clsx";
|
|
7
7
|
|
|
8
|
-
const GlobalNav = (
|
|
8
|
+
const GlobalNav = ({
|
|
9
|
+
type = "sidebar",
|
|
10
|
+
navMenu,
|
|
11
|
+
}: {
|
|
12
|
+
type?: "sidebar" | "topmenu" | "mobile";
|
|
13
|
+
navMenu: {
|
|
14
|
+
name: string;
|
|
15
|
+
path: string;
|
|
16
|
+
subItems?: { name: string; path: string }[];
|
|
17
|
+
}[];
|
|
18
|
+
}) => {
|
|
9
19
|
const [windowSize, setWindowSize] = useState<number>(); // 초기값 설정
|
|
10
|
-
|
|
11
|
-
const [toggleMenu, setToggleMenu] = useState<boolean>(false);
|
|
12
|
-
|
|
20
|
+
const [toggleMenu, setToggleMenu] = useState<boolean>(true);
|
|
13
21
|
const pathname = usePathname();
|
|
14
|
-
const
|
|
15
|
-
{ name: "시작하기", path: "/" },
|
|
16
|
-
{ name: "설치하기", path: "/install" },
|
|
17
|
-
{ name: "입력창", path: "/component/input" },
|
|
18
|
-
{ name: "선택창", path: "/component/select" },
|
|
19
|
-
{ name: "체크박스", path: "/component/checkbox" },
|
|
20
|
-
{ name: "라디오", path: "/component/radio" },
|
|
21
|
-
{ name: "버튼", path: "/component/button" },
|
|
22
|
-
];
|
|
22
|
+
const [openSubMenu, setOpenSubMenu] = useState<Record<string, boolean>>({});
|
|
23
23
|
|
|
24
24
|
// 윈도우 사이즈 추적
|
|
25
25
|
useEffect(() => {
|
|
@@ -35,21 +35,28 @@ const GlobalNav = () => {
|
|
|
35
35
|
// 초기 토글 값 설정
|
|
36
36
|
useEffect(() => {
|
|
37
37
|
if (window.innerWidth > 768) {
|
|
38
|
-
setToggleMenu(true);
|
|
39
38
|
} else {
|
|
40
39
|
setToggleMenu(false);
|
|
41
40
|
}
|
|
42
|
-
|
|
43
|
-
// console.log(
|
|
44
|
-
// `one Time Work!: ${toggleMenu}, windowSize:${window.innerWidth}`
|
|
45
|
-
// );
|
|
46
41
|
}, []);
|
|
47
42
|
|
|
43
|
+
const toggleSubMenu = (menuName: string) => {
|
|
44
|
+
setOpenSubMenu((prev) => ({
|
|
45
|
+
...prev,
|
|
46
|
+
[menuName]: !prev[menuName],
|
|
47
|
+
}));
|
|
48
|
+
};
|
|
49
|
+
|
|
48
50
|
return (
|
|
49
51
|
<>
|
|
50
52
|
<nav
|
|
51
|
-
className={
|
|
53
|
+
className={clsx(
|
|
54
|
+
`fixed top-0 md:relative w-full bg-[var(--background-card)] text-text-standard z-20 left-0 ${toggleMenu ? "block" : "hidden"}`,
|
|
55
|
+
{ "h-full max-w-80": type == "sidebar" },
|
|
56
|
+
{ "h-auto flex flex-row items-center": type == "topmenu" }
|
|
57
|
+
)}
|
|
52
58
|
>
|
|
59
|
+
{/* brand */}
|
|
53
60
|
<div className="brand">
|
|
54
61
|
<div className="flex items-center justify-between">
|
|
55
62
|
<Link href={"/"}>
|
|
@@ -61,41 +68,106 @@ const GlobalNav = () => {
|
|
|
61
68
|
draggable="false"
|
|
62
69
|
></Image>
|
|
63
70
|
</Link>
|
|
64
|
-
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
{type == "sidebar" ? (
|
|
72
|
+
<button className="p-4 cursor-pointer">
|
|
73
|
+
<span
|
|
74
|
+
className="material-symbols-outlined text-[var(--text-standard)]"
|
|
75
|
+
onClick={() => setToggleMenu(false)}
|
|
76
|
+
style={{ fontSize: "16px" }}
|
|
77
|
+
>
|
|
78
|
+
close
|
|
79
|
+
</span>
|
|
80
|
+
</button>
|
|
81
|
+
) : null}
|
|
73
82
|
</div>
|
|
74
83
|
</div>
|
|
75
84
|
|
|
76
|
-
|
|
85
|
+
{/* List */}
|
|
86
|
+
<ul
|
|
87
|
+
className={clsx("", {
|
|
88
|
+
"flex flex-row": type == "topmenu",
|
|
89
|
+
})}
|
|
90
|
+
>
|
|
77
91
|
{navMenu.map((menu) => {
|
|
78
92
|
return (
|
|
79
|
-
<li key={menu.name}>
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
93
|
+
<li className="relative group" key={menu.name}>
|
|
94
|
+
<div className={clsx("flex justify-between items-center pr-4")}>
|
|
95
|
+
<Link
|
|
96
|
+
href={menu.path}
|
|
97
|
+
className={clsx(
|
|
98
|
+
"px-4 py-2 block text-[var(--text-standard)]",
|
|
99
|
+
{
|
|
100
|
+
"text-primary":
|
|
101
|
+
menu.path == pathname ||
|
|
102
|
+
menu.subItems?.some((sub) => sub.path === pathname),
|
|
103
|
+
}
|
|
104
|
+
)}
|
|
105
|
+
>
|
|
106
|
+
{menu.name}
|
|
107
|
+
</Link>
|
|
108
|
+
{menu.subItems && type == "sidebar" ? (
|
|
109
|
+
<button
|
|
110
|
+
className="cursor-pointer"
|
|
111
|
+
onClick={() => toggleSubMenu(menu.name)}
|
|
112
|
+
>
|
|
113
|
+
<span
|
|
114
|
+
className={clsx(
|
|
115
|
+
"material-symbols-outlined text-[var(--text-sub)] transition-transform",
|
|
116
|
+
{
|
|
117
|
+
"rotate-90": openSubMenu[menu.name],
|
|
118
|
+
}
|
|
119
|
+
)}
|
|
120
|
+
style={{ fontSize: "16px" }}
|
|
121
|
+
>
|
|
122
|
+
keyboard_arrow_right
|
|
123
|
+
</span>
|
|
124
|
+
</button>
|
|
125
|
+
) : null}
|
|
126
|
+
</div>
|
|
127
|
+
{menu.subItems ? (
|
|
128
|
+
<ul
|
|
129
|
+
className={clsx(
|
|
130
|
+
"block",
|
|
131
|
+
{
|
|
132
|
+
"hidden absolute top-[42px] group-hover:block hover:block bg-[var(--background-card)]":
|
|
133
|
+
type == "topmenu",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
hidden: !openSubMenu[menu.name] && type == "sidebar",
|
|
137
|
+
}
|
|
138
|
+
)}
|
|
139
|
+
>
|
|
140
|
+
{menu.subItems.map((submenu) => {
|
|
141
|
+
return (
|
|
142
|
+
<li key={submenu.name}>
|
|
143
|
+
<Link
|
|
144
|
+
className={clsx(
|
|
145
|
+
"px-4 py-2 block text-[var(--text-standard)]",
|
|
146
|
+
{
|
|
147
|
+
"text-primary": submenu.path == pathname,
|
|
148
|
+
},
|
|
149
|
+
{ "pl-12 ": type == "sidebar" }
|
|
150
|
+
)}
|
|
151
|
+
href={submenu.path}
|
|
152
|
+
>
|
|
153
|
+
{submenu.name}
|
|
154
|
+
</Link>
|
|
155
|
+
</li>
|
|
156
|
+
);
|
|
157
|
+
})}
|
|
158
|
+
</ul>
|
|
159
|
+
) : null}
|
|
88
160
|
</li>
|
|
89
161
|
);
|
|
90
162
|
})}
|
|
91
163
|
</ul>
|
|
92
164
|
</nav>
|
|
93
165
|
<button
|
|
94
|
-
className={`fixed shadow-md rounded-full bg-[var(--background-card)] w-12 h-12 top-6 left-6 z-10 ${toggleMenu ? "hidden" : "block"} `}
|
|
166
|
+
className={`fixed shadow-md rounded-full bg-[var(--background-card)] w-12 h-12 top-6 left-6 z-10 cursor-pointer ${toggleMenu ? "hidden" : "block"} `}
|
|
167
|
+
onClick={() => setToggleMenu(true)}
|
|
95
168
|
>
|
|
96
169
|
<span
|
|
97
|
-
className="material-symbols-outlined"
|
|
98
|
-
onClick={() => setToggleMenu(true)}
|
|
170
|
+
className="material-symbols-outlined text-[var(--text-standard)]"
|
|
99
171
|
style={{ fontSize: "16px" }}
|
|
100
172
|
>
|
|
101
173
|
menu
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
|
|
5
|
+
const Pagination = ({
|
|
6
|
+
pageRange,
|
|
7
|
+
pageViewRange,
|
|
8
|
+
totalCount,
|
|
9
|
+
pageTotal,
|
|
10
|
+
currentPage,
|
|
11
|
+
onPageChange,
|
|
12
|
+
}: {
|
|
13
|
+
currentPage: number;
|
|
14
|
+
onPageChange: (page: number) => void;
|
|
15
|
+
/** 페이지당 데이터 개수 정보를 받습니다. */
|
|
16
|
+
pageViewRange: number;
|
|
17
|
+
/** 페이지네이션을 보여줄 최대 개수정보를 받습니다. */
|
|
18
|
+
pageRange: number;
|
|
19
|
+
/** totlaCount로 받아 계산하거나 전체 페이지네이션의 토탈 정보를 받습니다. */
|
|
20
|
+
totalCount: number;
|
|
21
|
+
/** totlaCount로 받아 계산하거나 전체 페이지네이션의 토탈 정보를 받습니다. */
|
|
22
|
+
pageTotal?: number;
|
|
23
|
+
}) => {
|
|
24
|
+
const totalPages = pageTotal
|
|
25
|
+
? pageTotal
|
|
26
|
+
: totalCount
|
|
27
|
+
? Math.ceil(totalCount / pageViewRange)
|
|
28
|
+
: 0;
|
|
29
|
+
const [pageStart, setPageStart] = useState(1);
|
|
30
|
+
const scrollTop = () => {
|
|
31
|
+
setTimeout(() => {
|
|
32
|
+
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
33
|
+
}, 0);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div className="text-[#999] flex border-[#666] border-b">
|
|
38
|
+
<button
|
|
39
|
+
disabled={pageStart === 1}
|
|
40
|
+
onClick={() => {
|
|
41
|
+
scrollTop();
|
|
42
|
+
const newStart = Math.max(1, pageStart - pageRange);
|
|
43
|
+
setPageStart(newStart);
|
|
44
|
+
onPageChange(newStart);
|
|
45
|
+
}}
|
|
46
|
+
className="px-2 py-8 cursor-pointer disabled:opacity-40"
|
|
47
|
+
>
|
|
48
|
+
<span className="material-symbols-outlined">keyboard_arrow_left</span>
|
|
49
|
+
</button>
|
|
50
|
+
<ul className="grid grid-cols-5 w-full">
|
|
51
|
+
{[...Array(pageRange)].map((_, i) => {
|
|
52
|
+
const page = pageStart + i;
|
|
53
|
+
|
|
54
|
+
const isFirstIndex = i === 0;
|
|
55
|
+
const isLastIndex = i === pageRange - 1;
|
|
56
|
+
const isLastPageInRange = page === totalPages;
|
|
57
|
+
|
|
58
|
+
if (page > totalPages) return null;
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<li
|
|
62
|
+
key={page}
|
|
63
|
+
onClick={() => {
|
|
64
|
+
onPageChange(page);
|
|
65
|
+
|
|
66
|
+
// 첫 번째 인덱스 클릭 시 이전 범위로
|
|
67
|
+
if (isFirstIndex && pageStart > 1) {
|
|
68
|
+
setPageStart(Math.max(1, pageStart - pageRange + 1));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 마지막 인덱스 클릭 시 다음 범위로
|
|
72
|
+
if (isLastIndex && !isLastPageInRange) {
|
|
73
|
+
setPageStart(pageStart + pageRange - 1);
|
|
74
|
+
}
|
|
75
|
+
scrollTop();
|
|
76
|
+
}}
|
|
77
|
+
className={` w-full text-center py-8 cursor-pointer ${
|
|
78
|
+
currentPage === page ? "text-primary" : ""
|
|
79
|
+
}`}
|
|
80
|
+
>
|
|
81
|
+
{page}
|
|
82
|
+
</li>
|
|
83
|
+
);
|
|
84
|
+
})}
|
|
85
|
+
</ul>
|
|
86
|
+
<button
|
|
87
|
+
disabled={pageStart + pageRange > totalPages}
|
|
88
|
+
onClick={() => {
|
|
89
|
+
scrollTop();
|
|
90
|
+
const newStart = pageStart + pageRange;
|
|
91
|
+
if (newStart <= totalPages) {
|
|
92
|
+
setPageStart(newStart);
|
|
93
|
+
onPageChange(newStart);
|
|
94
|
+
}
|
|
95
|
+
}}
|
|
96
|
+
className="px-2 py-8 cursor-pointer disabled:opacity-40"
|
|
97
|
+
>
|
|
98
|
+
<span className="material-symbols-outlined">keyboard_arrow_right</span>
|
|
99
|
+
</button>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export default Pagination;
|
|
@@ -6,23 +6,47 @@ import { useGSAP } from "@gsap/react";
|
|
|
6
6
|
|
|
7
7
|
const Tab = ({
|
|
8
8
|
tabs,
|
|
9
|
-
tabClass,
|
|
10
|
-
tabNavItemClass,
|
|
11
|
-
tabNavItemActiveClass,
|
|
9
|
+
tabClass = "w-full bg-[var(--background-wrapper)] border-b border-[var(--color-border)] relative z-0 h-[57px]",
|
|
10
|
+
tabNavItemClass = "p-4",
|
|
11
|
+
tabNavItemActiveClass = "bg-[var(--background-card)] border border-[var(--color-border)] border-b-[var(--background-card)] relative z-10",
|
|
12
12
|
tabContents,
|
|
13
|
-
tabContentClass,
|
|
14
|
-
tabContentItem,
|
|
13
|
+
tabContentClass = "p-4 border border-t-0 border-[var(--color-border)] -mt-[1px]",
|
|
14
|
+
tabContentItem = "text-[var(--text-standard)]",
|
|
15
15
|
expectionValue,
|
|
16
16
|
expectionLayout,
|
|
17
17
|
}: {
|
|
18
|
-
|
|
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를 받습니다.*/
|
|
19
29
|
tabClass?: string;
|
|
30
|
+
/** 탭 메뉴의 각각의 tailwinds css 스타일 지정을 위한 class를 받습니다.*/
|
|
20
31
|
tabNavItemClass?: string;
|
|
32
|
+
/** 탭 메뉴의 활성화 상태의 tailwinds css 스타일 지정을 위한 class를 받습니다.*/
|
|
21
33
|
tabNavItemActiveClass?: string;
|
|
22
|
-
|
|
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를 받습니다. */
|
|
23
44
|
tabContentClass?: string;
|
|
45
|
+
/** 탭 메뉴의 콘텐츠의 각 개별 요소를 위한 tailwinds css 스타일 지정을 위한 class를 받습니다. */
|
|
24
46
|
tabContentItem?: string;
|
|
47
|
+
/** 예외 레이아웃을 사용할 콘텐츠의 tag값을 입력받습니다. */
|
|
25
48
|
expectionValue?: string;
|
|
49
|
+
/** 예외 레이아웃에 사용하기 위한 tailwinds css 스타일 지정을 위한 class를 받습니다. */
|
|
26
50
|
expectionLayout?: string;
|
|
27
51
|
}) => {
|
|
28
52
|
const [activeContent, setActiveContent] = useState(tabs[0].target);
|
|
@@ -0,0 +1,63 @@
|
|
|
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="p-4 text-center">
|
|
10
|
+
<p className="text-[var(--text-standard)]">표시할 데이터가 없습니다.</p>
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const headers = Object.keys(tableData[0]);
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className="overflow-auto w-full">
|
|
19
|
+
<table className="min-w-full border border-[var(--color-border)] rounded]">
|
|
20
|
+
<thead className="bg-[var(--table-header-background)]">
|
|
21
|
+
<tr>
|
|
22
|
+
{/* 헤더 배열을 순회하며 각 헤더에 대한 <th>태그를 생성합니다. */}
|
|
23
|
+
{headers.map((header, index) => {
|
|
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
|
+
>
|
|
29
|
+
{header}
|
|
30
|
+
</th>
|
|
31
|
+
);
|
|
32
|
+
})}
|
|
33
|
+
</tr>
|
|
34
|
+
</thead>
|
|
35
|
+
{/* 테이블 바디 부분 */}
|
|
36
|
+
<tbody className={`divide-y divide-[var(--color-border)] `}>
|
|
37
|
+
{/* tableData 배열을 순회하며 각 데이터 객체(행)에 대한 <tr> 태그를 생성합니다. */}
|
|
38
|
+
{tableData.map((row, rowIndex) => {
|
|
39
|
+
return (
|
|
40
|
+
<tr
|
|
41
|
+
key={rowIndex}
|
|
42
|
+
className={` ${rowIndex % 2 === 0 ? `bg-[var(--table-body-background)]` : `bg-[var(--table-body-background-even)]`}`}
|
|
43
|
+
>
|
|
44
|
+
{/* 각 행 내에서 헤더 배열을 다시 순회하며 해당 헤더에 맞는 셀(<td>)을 생성합니다. */}
|
|
45
|
+
{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
|
+
>
|
|
50
|
+
{/* 현재 행 객체(row)에서 해당 헤더(키)에 해당하는 값을 가져와 셀에 표시합니다. */}
|
|
51
|
+
{row[header]}
|
|
52
|
+
</td>
|
|
53
|
+
))}
|
|
54
|
+
</tr>
|
|
55
|
+
);
|
|
56
|
+
})}
|
|
57
|
+
</tbody>
|
|
58
|
+
</table>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export default Table;
|
|
@@ -97,7 +97,7 @@ const ToastItem = ({
|
|
|
97
97
|
return (
|
|
98
98
|
<div
|
|
99
99
|
ref={elRef}
|
|
100
|
-
className="bg-
|
|
100
|
+
className="bg-[var(--background-card)] border border-[var(--color-border)] rounded text-white p-4 shadow-md min-w-[250px]"
|
|
101
101
|
>
|
|
102
102
|
<p>{message.message}</p>
|
|
103
103
|
{message.desc ? <p className="text-sm">{message.desc}</p> : null}
|
package/cli/add.js
CHANGED
package/package.json
CHANGED
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "atomsystem",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"type": "module",
|
|
5
|
-
"homepage": "https://system.atomground.com",
|
|
6
|
-
"bin": {
|
|
7
|
-
"atomsystem-init": "cli/init.js",
|
|
8
|
-
"atomsystem-add": "cli/add.js"
|
|
9
|
-
},
|
|
10
|
-
"files": [
|
|
11
|
-
"token.json",
|
|
12
|
-
"tokens",
|
|
13
|
-
"tailwind.config.ts",
|
|
14
|
-
"app/templates"
|
|
15
|
-
],
|
|
16
|
-
"scripts": {
|
|
17
|
-
"dev": "node build/build-tokens.js && next dev",
|
|
18
|
-
"build": "next build",
|
|
19
|
-
"start": "next start",
|
|
20
|
-
"lint": "next lint",
|
|
21
|
-
"token-build": "node build/build-tokens.js"
|
|
22
|
-
},
|
|
23
|
-
"peerDependencies": {
|
|
24
|
-
"@tokens-studio/sd-transforms": "^1.2.8",
|
|
25
|
-
"react": "^18.0.0",
|
|
26
|
-
"react-dom": "^18.0.0",
|
|
27
|
-
"sd-tailwindcss-transformer": "^2.0.0",
|
|
28
|
-
"style-dictionary": "^4.2.0"
|
|
29
|
-
},
|
|
30
|
-
"dependencies": {
|
|
31
|
-
"@gsap/react": "^2.1.2",
|
|
32
|
-
"@next/third-parties": "^15.0.3",
|
|
33
|
-
"@tailwindcss/postcss": "^4.0.17",
|
|
34
|
-
"@tokens-studio/sd-transforms": "^1.2.8",
|
|
35
|
-
"clsx": "^2.1.1",
|
|
36
|
-
"gsap": "^3.12.7",
|
|
37
|
-
"highlight.js": "^11.11.1",
|
|
38
|
-
"inquirer": "^12.4.2",
|
|
39
|
-
"next": "^15.2.4",
|
|
40
|
-
"react": "^18.0.0",
|
|
41
|
-
"react-dom": "^18.0.0",
|
|
42
|
-
"sd-tailwindcss-transformer": "^2.0.0",
|
|
43
|
-
"style-dictionary": "^4.2.0"
|
|
44
|
-
},
|
|
45
|
-
"devDependencies": {
|
|
46
|
-
"@types/node": "^20",
|
|
47
|
-
"@types/react": "^18",
|
|
48
|
-
"@types/react-dom": "^18",
|
|
49
|
-
"@vitest/browser": "^3.1.1",
|
|
50
|
-
"@vitest/coverage-v8": "^3.1.1",
|
|
51
|
-
"eslint": "^8",
|
|
52
|
-
"eslint-config-next": "15.0.3",
|
|
53
|
-
"playwright": "^1.51.1",
|
|
54
|
-
"postcss": "^8.5.3",
|
|
55
|
-
"tailwindcss": "^4.0.17",
|
|
56
|
-
"typescript": "^5",
|
|
57
|
-
"vitest": "^3.1.1"
|
|
58
|
-
}
|
|
59
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "atomsystem",
|
|
3
|
+
"version": "0.1.20",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"homepage": "https://system.atomground.com",
|
|
6
|
+
"bin": {
|
|
7
|
+
"atomsystem-init": "cli/init.js",
|
|
8
|
+
"atomsystem-add": "cli/add.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"token.json",
|
|
12
|
+
"tokens",
|
|
13
|
+
"tailwind.config.ts",
|
|
14
|
+
"app/templates"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"dev": "node build/build-tokens.js && next dev",
|
|
18
|
+
"build": "next build",
|
|
19
|
+
"start": "next start",
|
|
20
|
+
"lint": "next lint",
|
|
21
|
+
"token-build": "node build/build-tokens.js"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"@tokens-studio/sd-transforms": "^1.2.8",
|
|
25
|
+
"react": "^18.0.0",
|
|
26
|
+
"react-dom": "^18.0.0",
|
|
27
|
+
"sd-tailwindcss-transformer": "^2.0.0",
|
|
28
|
+
"style-dictionary": "^4.2.0"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@gsap/react": "^2.1.2",
|
|
32
|
+
"@next/third-parties": "^15.0.3",
|
|
33
|
+
"@tailwindcss/postcss": "^4.0.17",
|
|
34
|
+
"@tokens-studio/sd-transforms": "^1.2.8",
|
|
35
|
+
"clsx": "^2.1.1",
|
|
36
|
+
"gsap": "^3.12.7",
|
|
37
|
+
"highlight.js": "^11.11.1",
|
|
38
|
+
"inquirer": "^12.4.2",
|
|
39
|
+
"next": "^15.2.4",
|
|
40
|
+
"react": "^18.0.0",
|
|
41
|
+
"react-dom": "^18.0.0",
|
|
42
|
+
"sd-tailwindcss-transformer": "^2.0.0",
|
|
43
|
+
"style-dictionary": "^4.2.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^20",
|
|
47
|
+
"@types/react": "^18",
|
|
48
|
+
"@types/react-dom": "^18",
|
|
49
|
+
"@vitest/browser": "^3.1.1",
|
|
50
|
+
"@vitest/coverage-v8": "^3.1.1",
|
|
51
|
+
"eslint": "^8",
|
|
52
|
+
"eslint-config-next": "15.0.3",
|
|
53
|
+
"playwright": "^1.51.1",
|
|
54
|
+
"postcss": "^8.5.3",
|
|
55
|
+
"tailwindcss": "^4.0.17",
|
|
56
|
+
"typescript": "^5",
|
|
57
|
+
"vitest": "^3.1.1"
|
|
58
|
+
}
|
|
59
|
+
}
|
package/token.json
CHANGED
|
@@ -214,5 +214,41 @@
|
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
|
+
},
|
|
218
|
+
"table": {
|
|
219
|
+
"header": {
|
|
220
|
+
"background": {
|
|
221
|
+
"value": {
|
|
222
|
+
"light": "#444",
|
|
223
|
+
"dark": "#444"
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
"text": {
|
|
227
|
+
"value": {
|
|
228
|
+
"light": "#e6e6e6",
|
|
229
|
+
"dark": "#e6e6e6"
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
"body": {
|
|
234
|
+
"background": {
|
|
235
|
+
"value": {
|
|
236
|
+
"light": "#fafafa",
|
|
237
|
+
"dark": "#111"
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
"background-even": {
|
|
241
|
+
"value": {
|
|
242
|
+
"light": "#e6e6e6",
|
|
243
|
+
"dark": "#222"
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
"text": {
|
|
247
|
+
"value": {
|
|
248
|
+
"light": "#333",
|
|
249
|
+
"dark": "#e6e6e6"
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
217
253
|
}
|
|
218
254
|
}
|