atomsystem 0.1.38 → 0.1.40

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.
@@ -1,306 +1,169 @@
1
- "use client";
1
+ "use client"
2
+
2
3
  import Link from "next/link";
3
- import Image from "next/image";
4
- import { useEffect, useState } from "react";
5
4
  import { usePathname } from "next/navigation";
6
- import clsx from "clsx";
7
- import { useMediaQuery } from "react-responsive";
5
+ import gsap from 'gsap';
6
+ import { useGSAP } from '@gsap/react';
7
+ import { useRef, createContext, useContext, useState } from "react";
8
8
 
9
- interface NavItemsAttr {
10
- type?: "topnav" | "sidenav";
11
- navMenu: {
12
- name: string;
13
- path?: string;
14
- }[];
15
- }
9
+ // Icons
10
+ import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
16
11
 
17
- // Custom CSS classes are now used via css/navigation.css
18
12
 
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
- };
13
+ gsap.registerPlugin(useGSAP); // register the hook to avoid React version discrepancies
26
14
 
27
- const Navigation = ({
28
- children,
29
- id,
30
- className,
31
- type = "topnav",
32
- }: {
15
+ // Navigation을 베이스 interface로 정의
16
+ export interface Navigation {
33
17
  children: React.ReactNode;
34
- id?: string;
35
- className?: string;
36
- type?: "topnav" | "sidenav";
37
- }) => {
38
- const isMobile = useMediaQuery({ maxWidth: 767 });
39
-
40
- const mobileClose = () => {
41
- if (isMobile) {
42
- if (id) {
43
- handleNavToggle(id);
44
- }
45
- }
46
- };
18
+ }
47
19
 
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
- };
20
+ export interface NavLink extends Navigation {
21
+ href: string;
22
+ }
58
23
 
59
- const NavItems = ({ navMenu, type = "sidenav" }: NavItemsAttr) => {
60
- const pathname = usePathname();
24
+ /**
25
+ * NavContextProps: 네비게이션 상태를 하위 컴포넌트와 공유하기 위한 인터페이스
26
+ * - isOpen: 메뉴가 열려 있는지 여부
27
+ * - setOpen: 메뉴 상태를 변경하는 함수
28
+ */
29
+ interface NavContextProps {
30
+ isOpen: boolean;
31
+ setOpen: (open: boolean) => void;
32
+ }
61
33
 
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
- );
34
+ // NavContext: 상위 NavItem의 상태를 하 하위 NavTrigger와 NavContainer에 전달하는 통로(우체국)
35
+ const NavContext = createContext<NavContextProps | undefined>(undefined);
36
+
37
+ /**
38
+ * useNavContext: Context 정보를 쉽고 안전하게 꺼내 쓰기 위한 전용 커스텀 훅
39
+ * - 부모인 NavItem이 없을 경우(undefined)에도 에러가 나지 않도록 기본값을 반환하는 안전 장치 포함
40
+ */
41
+ const useNavContext = () => {
42
+ const context = useContext(NavContext);
43
+ if (!context) {
44
+ // 부모를 찾지 못했을 때 앱이 멈추지 않도록 기본 상태 반환
45
+ return { isOpen: false, setOpen: () => {} };
46
+ }
47
+ return context;
80
48
  };
81
49
 
82
- const NavToggleButton = ({
83
- children,
84
- targetId,
85
- className,
86
- }: {
87
- children: React.ReactNode;
88
- targetId: string;
89
- className?: string;
90
- }) => {
50
+ // 최상위 Navigation 레이아웃 컴포넌트
51
+ const Navigation = ({ children }: Navigation) => {
91
52
  return (
92
- <button className={className} onClick={() => handleNavToggle(targetId)}>
93
- {children}
94
- </button>
95
- );
96
- };
53
+ <nav className="nav-menu flex items-center justify-between">
54
+ {children}
55
+ </nav>
56
+ )
57
+ }
97
58
 
98
- const Sidebar = ({ navMenu, type = "sidenav" }: NavItemsAttr) => {
59
+ // 로고나 브랜드명을 위한 컴포넌트
60
+ const NavBrand = ({children, href}: NavLink) => {
99
61
  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
- };
62
+ <Link href={href} className="flex items-center h-full font-bold text-xl">{children}</Link>
63
+ )
64
+ }
126
65
 
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>>({});
66
+ /**
67
+ * NavItem: 개별 메뉴 항목 및 서브 메뉴의 상태를 관리하는 부모 컨테이너
68
+ * - 각각의 NavItem은 자신만의 isOpen 상태를 독립적으로 가짐
69
+ */
70
+ const NavItem = ({children}: Navigation) => {
71
+ const [isOpen, setIsOpen] = useState(false);
72
+ const setOpen = (open: boolean) => setIsOpen(open);
145
73
 
146
- // 윈도우 사이즈 추적
147
- useEffect(() => {
148
- const handleResize = () => {
149
- setWindowSize(window.innerWidth); // 윈도우 사이즈 업데이트
150
- };
74
+ // 마우스가 메뉴 전체 영역을 완전히 벗어날 때 호출 (메뉴 닫기)
75
+ const handleMouseLeave = () => setIsOpen(false);
151
76
 
152
- window.addEventListener("resize", handleResize); // 리사이즈 이벤트 리스너 추가
77
+ return (
78
+ <NavContext.Provider value={{ isOpen, setOpen }}>
79
+ <li
80
+ className={`nav-item`}
81
+ onMouseLeave={handleMouseLeave}
82
+ >
83
+ {children}
84
+ </li>
85
+ </NavContext.Provider>
86
+ )
87
+ }
153
88
 
154
- return () => window.removeEventListener("resize", handleResize); // 컴포넌트 언마운트 시 리스너 제거
155
- }, [windowSize]);
89
+ // 메뉴 항목들을 가로로 정렬해주는 리스트 컨테이너
90
+ const NavList = ({children}: Navigation) => {
91
+ return (
92
+ <ul className="flex items-center gap-0">{children}</ul>
93
+ )
94
+ }
156
95
 
157
- // 초기 토글 값 설정
158
- useEffect(() => {
159
- if (window.innerWidth > 768) {
160
- } else {
161
- setToggleMenu(false);
162
- }
163
- }, []);
96
+ // 일반적인 링크 메뉴 컴포넌트
97
+ const NavLink = ({children, href}: NavLink) => {
98
+ const pathname = usePathname();
99
+ const active = pathname === href;
164
100
 
165
- const toggleSubMenu = (menuName: string) => {
166
- setOpenSubMenu((prev) => ({
167
- ...prev,
168
- [menuName]: !prev[menuName],
169
- }));
170
- };
101
+ return (
102
+ <Link href={href} className={`nav-link ${active ? "active" : ""}`}>{children}</Link>
103
+ )
104
+ }
171
105
 
172
- const navType = type === "sidebar" ? "sidenav" : "topnav";
106
+ /**
107
+ * NavTrigger: 서브 메뉴를 여는 트리거 버튼
108
+ * - 마우스를 올렸을 때(onMouseEnter) 부모 NavItem의 상태를 열림(true)으로 변경
109
+ */
110
+ const NavTrigger = ({children}: Navigation) => {
111
+ const { isOpen, setOpen } = useNavContext();
112
+
113
+ // 트리거 영역에 진입했을 때만 메뉴 열기 실행
114
+ const handleMouseEnter = () => setOpen(true);
115
+
116
+ return (
117
+ <div
118
+ className="nav-link flex items-center gap-1"
119
+ onMouseEnter={handleMouseEnter}
120
+ >
121
+ {children}
122
+ {/* 상태에 따라 화살표 아이콘 회전 애니메이션 클래스 적용 */}
123
+ <KeyboardArrowDownIcon className={`nav-arrow ${isOpen ? "open" : ""}`} />
124
+ </div>
125
+ )
126
+ }
173
127
 
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>
128
+ /**
129
+ * NavContainer: 서브 메뉴의 실제 내용물이 담기는 컨테이너
130
+ * - GSAP을 사용하여 나타날 때 부드러운 애니메이션 효과 적용
131
+ */
132
+ const NavContainer = ({children}: Navigation) => {
133
+ const { isOpen } = useNavContext();
134
+ const containerContentRef = useRef<HTMLUListElement>(null);
135
+
136
+ // 메뉴가 열릴 때 실행되는 GSAP 애니메이션
137
+ useGSAP(() => {
138
+ if (isOpen && containerContentRef.current) {
139
+ gsap.fromTo(containerContentRef.current,
140
+ { opacity: 0, y: -10 }, // 시작 상태: 투명하고 살짝 위쪽
141
+ { opacity: 1, y: 0, duration: 0.2, ease: "power2.out" } // 도착 상태: 불투명하고 제자리
142
+ );
143
+ }
144
+ }, [isOpen]);
145
+
146
+
147
+ // 닫혀 있을 때는 아예 렌더링하지 않음
148
+ if (!isOpen) return null;
149
+
150
+ return (
151
+ <div className="nav-container">
152
+ {/* 실제 박스 형태를 가진 내부 리스트 */}
153
+ <ul ref={containerContentRef} className="nav-content">
154
+ {children}
155
+ </ul>
207
156
  </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 };
157
+ )
158
+ }
159
+
160
+
161
+ export {
162
+ Navigation,
163
+ NavBrand,
164
+ NavList,
165
+ NavItem,
166
+ NavLink,
167
+ NavTrigger,
168
+ NavContainer
169
+ }
@@ -1,69 +1,59 @@
1
-
2
-
3
- .nav-container.nav-topnav {
4
- background-color: var(--bg-topnav);
5
- color: var(--text-topnav-standard);
6
- border-color: var(--border-topnav);
7
- }
8
-
9
- .nav-container.nav-sidenav {
10
- background-color: var(--bg-sidenav);
11
- color: var(--text-sidenav-standard);
12
- border-color: var(--border-sidenav);
13
- }
14
-
15
- .nav-bg.nav-topnav {
16
- background-color: var(--bg-topnav);
17
- }
18
-
19
- .nav-bg.nav-sidenav {
20
- background-color: var(--bg-sidenav);
1
+ .nav-menu{
2
+ background-color: var(--bg-card);
3
+ padding: 0 var(--spacing-common-x);
4
+ border-bottom: 1px solid var(--border-standard);
5
+ position: relative;
21
6
  }
22
7
 
23
- .nav-border.nav-topnav {
24
- border-color: var(--border-topnav);
8
+ .nav-item{
9
+ display: block;
10
+ border-radius: var(--radius-common);
11
+ padding: var(--spacing-common-y) 0;
12
+ color: var(--text-standard);
25
13
  }
26
14
 
27
- .nav-border.nav-sidenav {
28
- border-color: var(--border-sidenav);
29
- }
30
-
31
- .nav-text-standard.nav-topnav {
32
- color: var(--text-topnav-standard);
33
- }
34
-
35
- .nav-text-standard.nav-sidenav {
36
- color: var(--text-sidenav-standard);
37
- }
38
-
39
- .nav-text-active.nav-topnav {
40
- color: var(--text-topnav-active);
41
- }
42
15
 
43
- .nav-text-active.nav-sidenav {
44
- color: var(--text-sidenav-active);
16
+ .nav-link{
17
+ display: block;
18
+ padding: var(--spacing-common-y) var(--spacing-common-x);
19
+ border-radius: var(--radius-common);
20
+ cursor: pointer;
45
21
  }
46
22
 
47
- .nav-text-sub.nav-topnav {
48
- color: var(--text-topnav-sub);
23
+ .nav-link:hover{
24
+ background-color: var(--bg-hover);
49
25
  }
50
26
 
51
- .nav-text-sub.nav-sidenav {
52
- color: var(--text-sidenav-sub);
27
+ .nav-link.active{
28
+ color: var(--color-primary);
53
29
  }
54
30
 
55
- .nav-padding.nav-topnav {
56
- padding: var(--spacing-topnav-y) var(--spacing-topnav-x);
31
+ .nav-container {
32
+ position: absolute;
33
+ padding: var(--spacing-common-y) 0;
34
+ top: 100%;
35
+ left: 0;
36
+ right: 0;
37
+ width: 100%;
38
+ z-index: 100;
57
39
  }
58
40
 
59
- .nav-padding.nav-sidenav {
60
- padding: var(--spacing-sidenav-y) var(--spacing-sidenav-x);
41
+ .nav-content {
42
+ display: flex;
43
+ width: 100%;
44
+ flex-wrap: wrap;
45
+ gap: var(--spacing-common-x);
46
+ background-color: var(--bg-card);
47
+ border: 1px solid var(--border-standard);
48
+ border-radius: var(--radius-common);
49
+ padding: var(--spacing-common-y) var(--spacing-common-x);
50
+ box-shadow: var(--shadow-standard);
61
51
  }
62
52
 
63
- .nav-gap.nav-topnav {
64
- gap: var(--spacing-topnav-gap);
53
+ .nav-arrow {
54
+ transition: transform 0.2s ease;
65
55
  }
66
56
 
67
- .nav-gap.nav-sidenav {
68
- gap: var(--spacing-sidenav-gap);
57
+ .nav-arrow.open {
58
+ transform: rotate(180deg);
69
59
  }