atomsystem 0.1.41 → 0.1.42
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,115 +1,207 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState } from "react";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, useState, ReactNode, useRef } from "react";
|
|
4
|
+
import gsap from 'gsap';
|
|
5
|
+
import { useGSAP } from '@gsap/react';
|
|
6
|
+
|
|
7
|
+
gsap.registerPlugin(useGSAP);
|
|
8
|
+
|
|
9
|
+
// Types
|
|
10
|
+
export interface CollapseProps {
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
type?: "accordion" | "collapse";
|
|
13
|
+
defaultActiveKeys?: (string | number)[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface CollapseContextValue {
|
|
17
|
+
type: "accordion" | "collapse";
|
|
18
|
+
activeKeys: Set<string | number>;
|
|
19
|
+
toggle: (key: string | number) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const CollapseContext = createContext<CollapseContextValue | undefined>(undefined);
|
|
23
|
+
|
|
24
|
+
const useCollapseContext = () => {
|
|
25
|
+
const context = useContext(CollapseContext);
|
|
26
|
+
if (!context) {
|
|
27
|
+
return { type: "collapse" as const, activeKeys: new Set<string | number>(), toggle: () => {} };
|
|
28
|
+
}
|
|
29
|
+
return context;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Provider
|
|
33
|
+
const CollapseProvider = ({
|
|
34
|
+
children,
|
|
35
|
+
type = "collapse",
|
|
36
|
+
defaultActiveKeys = []
|
|
37
|
+
}: CollapseProps) => {
|
|
38
|
+
const [activeKeys, setActiveKeys] = useState<Set<string | number>>(new Set(defaultActiveKeys));
|
|
39
|
+
|
|
40
|
+
const toggle = (key: string | number) => {
|
|
41
|
+
setActiveKeys((prev) => {
|
|
42
|
+
const next = new Set(prev);
|
|
43
|
+
if (next.has(key)) {
|
|
44
|
+
next.delete(key);
|
|
45
|
+
} else {
|
|
46
|
+
if (type === "accordion") {
|
|
47
|
+
next.clear();
|
|
48
|
+
}
|
|
49
|
+
next.add(key);
|
|
50
|
+
}
|
|
51
|
+
return next;
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<CollapseContext.Provider value={{ type, activeKeys, toggle }}>
|
|
57
|
+
{children}
|
|
58
|
+
</CollapseContext.Provider>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Root Container
|
|
63
|
+
const Collapse = ({ children, className }: { children: ReactNode; className?: string }) => {
|
|
64
|
+
return <div className={`collapse-group ${className || ""}`}>{children}</div>;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Item Context for child components
|
|
68
|
+
const ItemContext = createContext<{ value: string | number } | undefined>(undefined);
|
|
69
|
+
const useItemContext = () => {
|
|
70
|
+
const context = useContext(ItemContext);
|
|
71
|
+
if (!context) throw new Error("Collapse sub-components must be used within CollapseItem");
|
|
72
|
+
return context;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const CollapseItem = ({
|
|
76
|
+
value,
|
|
77
|
+
children,
|
|
78
|
+
className
|
|
79
|
+
}: {
|
|
80
|
+
value: string | number;
|
|
81
|
+
children: ReactNode;
|
|
82
|
+
className?: string;
|
|
83
|
+
}) => {
|
|
84
|
+
const { activeKeys } = useCollapseContext();
|
|
85
|
+
const isOpen = activeKeys.has(value);
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<ItemContext.Provider value={{ value }}>
|
|
89
|
+
<div
|
|
90
|
+
className={`collapse-item ${isOpen ? "open" : "closed"} ${className || ""}`}
|
|
91
|
+
data-state={isOpen ? "open" : "closed"}
|
|
92
|
+
>
|
|
93
|
+
{children}
|
|
94
|
+
</div>
|
|
95
|
+
</ItemContext.Provider>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const CollapseHeader = ({
|
|
100
|
+
children,
|
|
101
|
+
className
|
|
102
|
+
}: {
|
|
103
|
+
children: ReactNode;
|
|
104
|
+
className?: string;
|
|
105
|
+
}) => {
|
|
106
|
+
const { value } = useItemContext();
|
|
107
|
+
const { activeKeys, toggle } = useCollapseContext();
|
|
108
|
+
const isOpen = activeKeys.has(value);
|
|
109
|
+
const iconRef = useRef<HTMLSpanElement>(null);
|
|
110
|
+
|
|
111
|
+
useGSAP(() => {
|
|
112
|
+
if (iconRef.current) {
|
|
113
|
+
gsap.to(iconRef.current, {
|
|
114
|
+
rotation: isOpen ? 90 : 0,
|
|
115
|
+
duration: 0.4,
|
|
116
|
+
ease: "power2.inOut",
|
|
117
|
+
overwrite: true
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}, [isOpen]);
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<div
|
|
124
|
+
className={`collapse-header cursor-pointer flex items-center justify-between ${className || ""}`}
|
|
125
|
+
onClick={() => toggle(value)}
|
|
126
|
+
>
|
|
127
|
+
{children}
|
|
128
|
+
<span
|
|
129
|
+
ref={iconRef}
|
|
130
|
+
className="material-symbols-outlined collapse-icon"
|
|
131
|
+
>
|
|
132
|
+
keyboard_arrow_right
|
|
133
|
+
</span>
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const CollapseContent = ({
|
|
139
|
+
children,
|
|
140
|
+
className
|
|
141
|
+
}: {
|
|
142
|
+
children: ReactNode;
|
|
143
|
+
className?: string;
|
|
144
|
+
}) => {
|
|
145
|
+
const { value } = useItemContext();
|
|
146
|
+
const { activeKeys } = useCollapseContext();
|
|
147
|
+
const isOpen = activeKeys.has(value);
|
|
148
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
149
|
+
|
|
150
|
+
useGSAP(() => {
|
|
151
|
+
if (!contentRef.current) return;
|
|
152
|
+
|
|
153
|
+
if (isOpen) {
|
|
154
|
+
// 열기: 현재 높이가 0이거나 display: none인 상태에서도 부드럽게 시작할 수 있도록 처리
|
|
155
|
+
gsap.fromTo(contentRef.current,
|
|
156
|
+
{ height: 0, opacity: 0, display: "block" },
|
|
157
|
+
{
|
|
158
|
+
height: "auto",
|
|
159
|
+
opacity: 1,
|
|
160
|
+
duration: 0.4,
|
|
161
|
+
ease: "power2.out",
|
|
162
|
+
overwrite: true,
|
|
163
|
+
onComplete: () => {
|
|
164
|
+
// 애니메이션 완료 후 height를 auto로 고정하여 반응형 대응
|
|
165
|
+
if (contentRef.current) {
|
|
166
|
+
gsap.set(contentRef.current, { clearProps: "height" });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
);
|
|
171
|
+
} else {
|
|
172
|
+
// 닫기
|
|
173
|
+
gsap.to(contentRef.current, {
|
|
174
|
+
height: 0,
|
|
175
|
+
opacity: 0,
|
|
176
|
+
duration: 0.3,
|
|
177
|
+
ease: "power2.inOut",
|
|
178
|
+
overwrite: true,
|
|
179
|
+
onComplete: () => {
|
|
180
|
+
if (contentRef.current && !isOpen) {
|
|
181
|
+
gsap.set(contentRef.current, { display: "none" });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}, [isOpen]);
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<div
|
|
190
|
+
ref={contentRef}
|
|
191
|
+
className={`collapse-content overflow-hidden ${className || ""}`}
|
|
192
|
+
style={{ height: 0, opacity: 0, display: "none" }}
|
|
193
|
+
>
|
|
194
|
+
<div style={{ padding: "var(--spacing-common-x)" }}>
|
|
195
|
+
{children}
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
export {
|
|
202
|
+
Collapse,
|
|
203
|
+
CollapseProvider,
|
|
204
|
+
CollapseItem,
|
|
205
|
+
CollapseHeader,
|
|
206
|
+
CollapseContent
|
|
207
|
+
};
|
|
@@ -14,20 +14,11 @@
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
.collapse-icon {
|
|
17
|
-
transition: transform 0.3s ease;
|
|
18
17
|
color: var(--text-standard);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
.collapse-icon.open {
|
|
22
|
-
transform: rotate(90deg);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
.collapse-icon.closed {
|
|
26
|
-
transform: rotate(0deg);
|
|
18
|
+
display: inline-block; /* 회전 중심을 위해 적용 */
|
|
27
19
|
}
|
|
28
20
|
|
|
29
21
|
.collapse-content {
|
|
30
|
-
padding: var(--spacing-common-x);
|
|
31
22
|
color: var(--text-standard);
|
|
32
23
|
font-size: var(--text-body);
|
|
33
24
|
}
|