atomsystem 0.1.18 → 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.
@@ -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-fill-group border-none" : ""} ${!hasCardContent ? "p-4" : ""} rounded border ${className} overflow-auto`}
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 navMenu = [
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={`fixed top-0 md:relative w-full max-w-80 h-full bg-[var(--background-card)] text-text-standard z-20 left-0 ${toggleMenu ? "block" : "hidden"}`}
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
- <button className="p-4">
65
- <span
66
- className="material-symbols-outlined"
67
- onClick={() => setToggleMenu(false)}
68
- style={{ fontSize: "16px" }}
69
- >
70
- close
71
- </span>
72
- </button>
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
- <ul>
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
- <Link
81
- href={menu.path}
82
- className={clsx("px-4 py-2 block", {
83
- "text-primary": menu.path == pathname,
84
- })}
85
- >
86
- {menu.name}
87
- </Link>
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
- tabs: Array<{ id: string; target: string[]; tabItem: React.ReactNode }>;
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
- tabContents: Array<{ id: string; tag: string; content: React.ReactNode }>;
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-white/20 border border-primary rounded text-white p-4 shadow-md min-w-[250px]"
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
@@ -21,6 +21,10 @@ const components = [
21
21
  "GlobalNav",
22
22
  "Input",
23
23
  "Theme",
24
+ "Tab",
25
+ "Toast",
26
+ "Pagination",
27
+ "Table",
24
28
  ]; // 여기 추가할 컴포넌트 목록
25
29
 
26
30
  // 명령어에서 컴포넌트 이름 가져오기
package/package.json CHANGED
@@ -1,58 +1,59 @@
1
- {
2
- "name": "atomsystem",
3
- "version": "0.1.18",
4
- "type": "module",
5
- "bin": {
6
- "atomsystem-init": "cli/init.js",
7
- "atomsystem-add": "cli/add.js"
8
- },
9
- "files": [
10
- "token.json",
11
- "tokens",
12
- "tailwind.config.ts",
13
- "app/templates"
14
- ],
15
- "scripts": {
16
- "dev": "node build/build-tokens.js && next dev",
17
- "build": "next build",
18
- "start": "next start",
19
- "lint": "next lint",
20
- "token-build": "node build/build-tokens.js"
21
- },
22
- "peerDependencies": {
23
- "@tokens-studio/sd-transforms": "^1.2.8",
24
- "react": "^18.0.0",
25
- "react-dom": "^18.0.0",
26
- "sd-tailwindcss-transformer": "^2.0.0",
27
- "style-dictionary": "^4.2.0"
28
- },
29
- "dependencies": {
30
- "@gsap/react": "^2.1.2",
31
- "@next/third-parties": "^15.0.3",
32
- "@tailwindcss/postcss": "^4.0.17",
33
- "@tokens-studio/sd-transforms": "^1.2.8",
34
- "clsx": "^2.1.1",
35
- "gsap": "^3.12.7",
36
- "highlight.js": "^11.11.1",
37
- "inquirer": "^12.4.2",
38
- "next": "^15.2.4",
39
- "react": "^18.0.0",
40
- "react-dom": "^18.0.0",
41
- "sd-tailwindcss-transformer": "^2.0.0",
42
- "style-dictionary": "^4.2.0"
43
- },
44
- "devDependencies": {
45
- "@types/node": "^20",
46
- "@types/react": "^18",
47
- "@types/react-dom": "^18",
48
- "@vitest/browser": "^3.1.1",
49
- "@vitest/coverage-v8": "^3.1.1",
50
- "eslint": "^8",
51
- "eslint-config-next": "15.0.3",
52
- "playwright": "^1.51.1",
53
- "postcss": "^8.5.3",
54
- "tailwindcss": "^4.0.17",
55
- "typescript": "^5",
56
- "vitest": "^3.1.1"
57
- }
58
- }
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
  }