atomsystem 0.1.39 → 0.1.41
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/Navigation/Navigation.tsx +122 -6
- package/app/templates/Navigation/navigation.css +48 -5
- package/app/templates/Sidebar/Sidebar.tsx +139 -0
- package/build/css/_variables.css +250 -250
- package/build/typescript/theme.ts +5 -5
- package/dist/index.js +393 -0
- package/package.json +17 -5
- package/app/templates/Sidemenu/Sidemenu.tsx +0 -55
- package/cli/add.js +0 -143
- package/cli/init.js +0 -276
- /package/app/templates/{Sidemenu/sidemenu.css → Sidebar/sidebar.css} +0 -0
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
import Link from "next/link";
|
|
4
4
|
import { usePathname } from "next/navigation";
|
|
5
|
+
import gsap from 'gsap';
|
|
6
|
+
import { useGSAP } from '@gsap/react';
|
|
7
|
+
import { useRef, createContext, useContext, useState } from "react";
|
|
8
|
+
|
|
9
|
+
// Icons
|
|
10
|
+
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
gsap.registerPlugin(useGSAP); // register the hook to avoid React version discrepancies
|
|
5
14
|
|
|
6
15
|
// Navigation을 베이스 interface로 정의
|
|
7
16
|
export interface Navigation {
|
|
@@ -12,7 +21,33 @@ export interface NavLink extends Navigation {
|
|
|
12
21
|
href: string;
|
|
13
22
|
}
|
|
14
23
|
|
|
24
|
+
/**
|
|
25
|
+
* NavContextProps: 네비게이션 상태를 하위 컴포넌트와 공유하기 위한 인터페이스
|
|
26
|
+
* - isOpen: 메뉴가 열려 있는지 여부
|
|
27
|
+
* - setOpen: 메뉴 상태를 변경하는 함수
|
|
28
|
+
*/
|
|
29
|
+
interface NavContextProps {
|
|
30
|
+
isOpen: boolean;
|
|
31
|
+
setOpen: (open: boolean) => void;
|
|
32
|
+
}
|
|
15
33
|
|
|
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;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// 최상위 Navigation 레이아웃 컴포넌트
|
|
16
51
|
const Navigation = ({ children }: Navigation) => {
|
|
17
52
|
return (
|
|
18
53
|
<nav className="nav-menu flex items-center justify-between">
|
|
@@ -21,33 +56,114 @@ const Navigation = ({ children }: Navigation) => {
|
|
|
21
56
|
)
|
|
22
57
|
}
|
|
23
58
|
|
|
24
|
-
|
|
59
|
+
// 로고나 브랜드명을 위한 컴포넌트
|
|
25
60
|
const NavBrand = ({children, href}: NavLink) => {
|
|
26
61
|
return (
|
|
27
|
-
<Link href={href} className="flex items-center">{children}</Link>
|
|
62
|
+
<Link href={href} className="flex items-center h-full font-bold text-xl">{children}</Link>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
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);
|
|
73
|
+
|
|
74
|
+
// 마우스가 메뉴 전체 영역을 완전히 벗어날 때 호출 (메뉴 닫기)
|
|
75
|
+
const handleMouseLeave = () => setIsOpen(false);
|
|
76
|
+
|
|
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>
|
|
28
86
|
)
|
|
29
87
|
}
|
|
30
88
|
|
|
89
|
+
// 메뉴 항목들을 가로로 정렬해주는 리스트 컨테이너
|
|
31
90
|
const NavList = ({children}: Navigation) => {
|
|
32
91
|
return (
|
|
33
92
|
<ul className="flex items-center gap-0">{children}</ul>
|
|
34
93
|
)
|
|
35
94
|
}
|
|
36
95
|
|
|
37
|
-
|
|
38
|
-
const
|
|
96
|
+
// 일반적인 링크 메뉴 컴포넌트
|
|
97
|
+
const NavLink = ({children, href}: NavLink) => {
|
|
39
98
|
const pathname = usePathname();
|
|
40
99
|
const active = pathname === href;
|
|
41
100
|
|
|
42
101
|
return (
|
|
43
|
-
<Link href={href} className={`nav-
|
|
102
|
+
<Link href={href} className={`nav-link ${active ? "active" : ""}`}>{children}</Link>
|
|
44
103
|
)
|
|
45
104
|
}
|
|
46
105
|
|
|
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
|
+
}
|
|
127
|
+
|
|
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>
|
|
156
|
+
</div>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
47
160
|
|
|
48
161
|
export {
|
|
49
162
|
Navigation,
|
|
50
163
|
NavBrand,
|
|
51
164
|
NavList,
|
|
52
|
-
NavItem
|
|
165
|
+
NavItem,
|
|
166
|
+
NavLink,
|
|
167
|
+
NavTrigger,
|
|
168
|
+
NavContainer
|
|
53
169
|
}
|
|
@@ -1,16 +1,59 @@
|
|
|
1
1
|
.nav-menu{
|
|
2
2
|
background-color: var(--bg-card);
|
|
3
|
+
padding: 0 var(--spacing-common-x);
|
|
3
4
|
border-bottom: 1px solid var(--border-standard);
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
position: relative;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
.nav-item{
|
|
9
|
+
display: block;
|
|
9
10
|
border-radius: var(--radius-common);
|
|
11
|
+
padding: var(--spacing-common-y) 0;
|
|
12
|
+
color: var(--text-standard);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
.nav-link{
|
|
17
|
+
display: block;
|
|
10
18
|
padding: var(--spacing-common-y) var(--spacing-common-x);
|
|
11
|
-
|
|
19
|
+
border-radius: var(--radius-common);
|
|
20
|
+
cursor: pointer;
|
|
12
21
|
}
|
|
13
22
|
|
|
14
|
-
.nav-
|
|
23
|
+
.nav-link:hover{
|
|
15
24
|
background-color: var(--bg-hover);
|
|
16
|
-
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.nav-link.active{
|
|
28
|
+
color: var(--color-primary);
|
|
29
|
+
}
|
|
30
|
+
|
|
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;
|
|
39
|
+
}
|
|
40
|
+
|
|
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);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.nav-arrow {
|
|
54
|
+
transition: transform 0.2s ease;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.nav-arrow.open {
|
|
58
|
+
transform: rotate(180deg);
|
|
59
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
5
|
+
import gsap from 'gsap';
|
|
6
|
+
import { useGSAP } from '@gsap/react';
|
|
7
|
+
import { createContext, useContext, useRef, useState } from "react";
|
|
8
|
+
|
|
9
|
+
// Icons
|
|
10
|
+
import MenuIcon from '@mui/icons-material/Menu';
|
|
11
|
+
|
|
12
|
+
gsap.registerPlugin(useGSAP); // register the hook to avoid React version discrepancies
|
|
13
|
+
|
|
14
|
+
// Navigation을 베이스 interface로 정의
|
|
15
|
+
export interface Sidebar {
|
|
16
|
+
children: React.ReactNode;
|
|
17
|
+
minWidth?: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface NavLink {
|
|
21
|
+
children: React.ReactNode;
|
|
22
|
+
href: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface SideContext {
|
|
26
|
+
isOpen: boolean;
|
|
27
|
+
setToggle: (open: boolean) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const SideContext = createContext<SideContext | undefined>(undefined);
|
|
31
|
+
|
|
32
|
+
const useSideContext = () => {
|
|
33
|
+
const context = useContext(SideContext);
|
|
34
|
+
if (!context) {
|
|
35
|
+
return { isOpen: false, setToggle: () => {} };
|
|
36
|
+
}
|
|
37
|
+
return context;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const SidebarProvider = ({ children }: { children: React.ReactNode }) => {
|
|
41
|
+
const [isOpen, setIsOpen] = useState(true);
|
|
42
|
+
const setToggle = (open: boolean) => setIsOpen(open);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<SideContext.Provider value={{ isOpen, setToggle }}>
|
|
46
|
+
{children}
|
|
47
|
+
</SideContext.Provider>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const SidebarToggle = () => {
|
|
52
|
+
const { isOpen, setToggle } = useSideContext();
|
|
53
|
+
return (
|
|
54
|
+
<button className="cursor-pointer" onClick={() => setToggle(!isOpen)}><MenuIcon className="text-text-sub" sx={{fontSize: "20px"}}/></button>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const Sidebar = ({ children, minWidth }: Sidebar) => {
|
|
59
|
+
const { isOpen } = useSideContext();
|
|
60
|
+
const sidebarRef = useRef<HTMLDivElement>(null);
|
|
61
|
+
|
|
62
|
+
useGSAP(() => {
|
|
63
|
+
if (sidebarRef.current) {
|
|
64
|
+
if (isOpen) {
|
|
65
|
+
// 열릴 때: 나타나면서 제자리로
|
|
66
|
+
gsap.to(sidebarRef.current, {
|
|
67
|
+
display: "flex",
|
|
68
|
+
opacity: 1,
|
|
69
|
+
x: 0,
|
|
70
|
+
width: minWidth,
|
|
71
|
+
minWidth: minWidth,
|
|
72
|
+
duration: 0.3,
|
|
73
|
+
ease: "power2.out"
|
|
74
|
+
});
|
|
75
|
+
} else {
|
|
76
|
+
// 닫힐 때: 사라지면서 왼쪽으로, 너비도 0으로
|
|
77
|
+
gsap.to(sidebarRef.current, {
|
|
78
|
+
opacity: 0,
|
|
79
|
+
x: -20,
|
|
80
|
+
width: 0,
|
|
81
|
+
minWidth: 0,
|
|
82
|
+
duration: 0.3,
|
|
83
|
+
ease: "power2.in",
|
|
84
|
+
onComplete: () => {
|
|
85
|
+
gsap.set(sidebarRef.current, { display: "none" });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}, [isOpen]);
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<aside ref={sidebarRef} className={`nav-side flex flex-col overflow-auto h-full`} style={{ width: isOpen ? minWidth : 0, minWidth: isOpen ? minWidth : 0 }}>
|
|
95
|
+
{children}
|
|
96
|
+
</aside>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
const SideNavBrand = ({children, href}: NavLink) => {
|
|
102
|
+
return (
|
|
103
|
+
<Link href={href} className="flex items-center">{children}</Link>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const SideNavList = ({children}: Sidebar) => {
|
|
108
|
+
return (
|
|
109
|
+
<ul className="nav-list flex flex-col">{children}</ul>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
const SideNavItem = ({children}: Sidebar) => {
|
|
115
|
+
return (
|
|
116
|
+
<li>{children}</li>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const SideNavLink = ({children, href}: NavLink) => {
|
|
121
|
+
|
|
122
|
+
const pathname = usePathname();
|
|
123
|
+
const active = pathname === href;
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<Link href={href} className={`nav-link text-nowrap ${active ? "active" : ""}`}>{children}</Link>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
export {
|
|
132
|
+
Sidebar,
|
|
133
|
+
SidebarProvider,
|
|
134
|
+
SidebarToggle,
|
|
135
|
+
SideNavBrand,
|
|
136
|
+
SideNavList,
|
|
137
|
+
SideNavItem,
|
|
138
|
+
SideNavLink
|
|
139
|
+
}
|