@edu-tosel/design 1.0.349 → 1.0.351

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.
@@ -3,7 +3,7 @@ import React, { Fragment } from "react";
3
3
  import { useActionStore } from "../../store";
4
4
  function Show({ actions, children }) {
5
5
  const { events } = useActionStore();
6
- return (_jsxs(_Fragment, { children: [children, actions?.map(([flag, Component], index) => {
6
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "no-scrollbar", children: children }), actions?.map(([flag, Component], index) => {
7
7
  const isVisible = typeof flag === "boolean"
8
8
  ? flag
9
9
  : events?.some(({ event }) => event === flag);
@@ -80,7 +80,7 @@ export default function Announcement({ banners, browse }) {
80
80
  //adjust the responsive right margin of scroller
81
81
  const cardDeck = {
82
82
  displays: "inline-flex",
83
- spacings: "gap-5 xl:mr-[calc(100vw-900px)]",
83
+ spacings: "gap-5 xl:mr-[calc(100vw-1200px)]",
84
84
  };
85
85
  const buttonPositioning = {
86
86
  displays: "flex flex-row",
@@ -120,7 +120,7 @@ function Banner({ onClick, titles }) {
120
120
  sizes: "w-65 h-40 xxxs:w-76 md:w-80 md:h-40 rounded-lg md:rounded-lg",
121
121
  backgrounds: "bg-white",
122
122
  hover: "hover:shadow-main-hover hover:scale-[1.03]",
123
- position: "xl:translate-x-[calc(50vw-770px)] md:translate-x-[56px]",
123
+ position: "xl:translate-x-[calc(50vw-665px)] md:translate-x-[56px]",
124
124
  transition: "duration-300",
125
125
  displays: "relative display-block overflow-hidden",
126
126
  fonts: "font-pretendard-var",
@@ -76,7 +76,7 @@ export default function NewsPaper({ banners, browse, option, }) {
76
76
  //adjust the responsive right margin of scroller
77
77
  const cardDeck = {
78
78
  displays: "inline-flex",
79
- spacings: "gap-5 xl:mr-[calc(100vw-900px)]",
79
+ spacings: "gap-5 xl:mr-[calc(100vw-1200px)]",
80
80
  };
81
81
  const buttonPositioning = {
82
82
  displays: "flex flex-row",
@@ -117,7 +117,7 @@ function Banner({ onClick, image, option }) {
117
117
  sizes: "w-65 h-40 xxxs:w-76 xxxs:h-47.5 md:w-120 md:h-75 rounded-xl md:rounded-2xl",
118
118
  backgrounds: background,
119
119
  hover: "hover:shadow-main-hover hover:scale-[1.03]",
120
- position: "xl:translate-x-[calc(50vw-770px)] md:translate-x-[56px]",
120
+ position: "xl:translate-x-[calc(50vw-665px)] md:translate-x-[56px]",
121
121
  transition: "duration-300",
122
122
  displays: "relative display-block overflow-hidden",
123
123
  fonts: "font-pretendard-var",
@@ -75,7 +75,7 @@ export default function Service({ banners }) {
75
75
  //adjust the responsive right margin of scroller
76
76
  const cardDeck = {
77
77
  displays: "inline-flex",
78
- spacings: "gap-5 xl:mr-[calc(100vw-900px)]",
78
+ spacings: "gap-5 xl:mr-[calc(100vw-1200px)]",
79
79
  };
80
80
  const buttonPositioning = {
81
81
  displays: "flex flex-row",
@@ -111,7 +111,7 @@ function Banner({ titles, onClick, image, option }) {
111
111
  sizes: "h-100 w-65 xxxs:w-76 md:w-100 md:h-125 rounded-xl md:rounded-2xl",
112
112
  backgrounds: background,
113
113
  hover: "hover:shadow-main-hover hover:scale-[1.03]",
114
- position: "xl:translate-x-[calc(50vw-770px)] md:translate-x-[56px]",
114
+ position: "xl:translate-x-[calc(50vw-665px)] md:translate-x-[56px]",
115
115
  transition: "duration-300",
116
116
  displays: "relative display-block overflow-hidden",
117
117
  fonts: "font-pretendard-var",
@@ -1,10 +1,37 @@
1
1
  import { ReactNode } from "react";
2
2
  import { Button as _Button, OnClick } from "../../../../interface";
3
+ type MenuItem = {
4
+ title: string;
5
+ description?: string;
6
+ tag?: {
7
+ title: string;
8
+ background?: string;
9
+ };
10
+ onClick?: OnClick;
11
+ };
12
+ type MenuData = {
13
+ title: {
14
+ title: string;
15
+ image?: string;
16
+ description?: string;
17
+ button?: {
18
+ title: string;
19
+ style?: string;
20
+ align?: "left" | "right" | "center";
21
+ onClick?: OnClick;
22
+ };
23
+ };
24
+ content: MenuItem[];
25
+ };
26
+ type ExtendedButton = _Button & {
27
+ menu?: MenuData;
28
+ };
3
29
  export default function Header({ logo, rightButton, contents, }: {
4
30
  logo: {
5
31
  node: ReactNode;
6
32
  onClick?: OnClick;
7
33
  };
8
34
  rightButton?: React.ReactNode;
9
- contents: _Button[];
35
+ contents: ExtendedButton[];
10
36
  }): import("react/jsx-runtime").JSX.Element;
37
+ export {};
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useState, useEffect, useRef } from "react";
3
3
  import { cn } from "../../../../util";
4
4
  import { useResponsive } from "../../../../hook";
@@ -6,13 +6,49 @@ import { gsap } from "gsap";
6
6
  export default function Header({ logo, rightButton, contents, }) {
7
7
  const [isExpanded, setIsExpanded] = useState(false);
8
8
  const [isInitialized, setIsInitialized] = useState(false);
9
+ const [hoveredIndex, setHoveredIndex] = useState(null);
10
+ const [mobileMenuIndex, setMobileMenuIndex] = useState(null);
9
11
  const headerRef = useRef(null);
10
12
  const menuContentRef = useRef(null);
13
+ const dropdownRef = useRef(null);
14
+ const mobileMenuRef = useRef(null);
15
+ const hoverTimeoutRef = useRef(null);
11
16
  const toggleHeight = () => {
12
17
  setIsExpanded(!isExpanded);
18
+ if (!isExpanded) {
19
+ setMobileMenuIndex(null);
20
+ }
21
+ };
22
+ const handleMobileMenuClick = (index, e) => {
23
+ e?.stopPropagation(); // 이벤트 전파 방지
24
+ const button = contents[index];
25
+ if (button?.menu) {
26
+ // 메뉴가 있으면 토글 (함수형 업데이트 사용)
27
+ setMobileMenuIndex((prevIndex) => {
28
+ const newIndex = prevIndex === index ? null : index;
29
+ return newIndex;
30
+ });
31
+ }
32
+ else {
33
+ // 메뉴가 없으면 onClick 실행하고 닫기
34
+ button?.onClick?.();
35
+ setIsExpanded(false);
36
+ }
37
+ };
38
+ const closeMobileMenu = () => {
39
+ setIsExpanded(false);
40
+ setMobileMenuIndex(null);
13
41
  };
14
42
  const handleClickOutside = (event) => {
15
43
  const menuElement = document.getElementById("buttonArea");
44
+ const mobileMenuElement = mobileMenuRef.current;
45
+ // 모바일 메뉴가 열려있고, 클릭이 모바일 메뉴 내부나 버튼 영역이면 닫지 않음
46
+ if (isExpanded && !isMD) {
47
+ if (mobileMenuElement?.contains(event.target) ||
48
+ menuElement?.contains(event.target)) {
49
+ return;
50
+ }
51
+ }
16
52
  if (menuElement && !menuElement.contains(event.target)) {
17
53
  setIsExpanded(false);
18
54
  }
@@ -136,7 +172,7 @@ export default function Header({ logo, rightButton, contents, }) {
136
172
  boundaries: "border-gray-light md:border-t-0 border-t-2",
137
173
  visibility: isMD
138
174
  ? "opacity-100 visible"
139
- : "opacity-0 invisible md:opacity-100 md:visible",
175
+ : "hidden md:opacity-100 md:visible",
140
176
  };
141
177
  const essenstialsWrapper = {
142
178
  displays: "flex flex-row items-center h-15",
@@ -147,5 +183,62 @@ export default function Header({ logo, rightButton, contents, }) {
147
183
  const menuButton = {
148
184
  displays: "flex justify-center items-center p-2",
149
185
  };
150
- return (_jsx("div", { ref: headerRef, className: cn(container), children: _jsxs("div", { className: cn(body), children: [_jsxs("div", { className: cn(leftWrapper), children: [_jsx("div", { onClick: logo.onClick, children: logo.node }), _jsxs("div", { className: cn(essenstialsWrapper), id: "buttonArea", children: [rightButton && rightButton, _jsx("button", { onClick: toggleHeight, className: cn(menuButton), children: _jsx("div", { className: "w-6 h-6", children: _jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", children: [!isExpanded && (_jsx("path", { id: "menu", fill: "#B0B8C1", d: "M4.118 6.2h16a1.2 1.2 0 100-2.4h-16a1.2 1.2 0 100 2.4m16 4.6h-16a1.2 1.2 0 100 2.4h16a1.2 1.2 0 100-2.4m0 7h-16a1.2 1.2 0 100 2.4h16a1.2 1.2 0 100-2.4", "fill-rule": "evenodd" })), isExpanded && (_jsx("path", { id: "close", fill: "#B0B8C1", "fill-rule": "evenodd", d: "M13.815 12l5.651-5.651a1.2 1.2 0 00-1.697-1.698l-5.651 5.652-5.652-5.652a1.201 1.201 0 00-1.697 1.698L10.421 12l-5.652 5.651a1.202 1.202 0 00.849 2.049c.307 0 .614-.117.848-.351l5.652-5.652 5.651 5.652a1.198 1.198 0 001.697 0 1.2 1.2 0 000-1.698L13.815 12z" }))] }) }) })] })] }), _jsxs("div", { ref: menuContentRef, className: cn(rightWrapper), children: [_jsx("div", { className: cn(buttonBoxWrapper), children: contents.map((button, index) => (_jsx("button", { onClick: button.onClick, className: cn(buttonBox), children: button.title }, index))) }), _jsx("div", { className: "hidden md:block", children: rightButton && rightButton })] })] }) }));
186
+ // 드롭다운 메뉴 애니메이션
187
+ useEffect(() => {
188
+ if (!dropdownRef.current)
189
+ return;
190
+ if (hoveredIndex !== null && contents[hoveredIndex]?.menu) {
191
+ gsap.fromTo(dropdownRef.current, { opacity: 0, y: -10 }, { opacity: 1, y: 0, duration: 0.2, ease: "power2.out" });
192
+ }
193
+ }, [hoveredIndex, contents]);
194
+ const handleMouseEnter = (index) => {
195
+ if (contents[index]?.menu && isMD) {
196
+ // 기존 타이머가 있으면 취소
197
+ if (hoverTimeoutRef.current) {
198
+ clearTimeout(hoverTimeoutRef.current);
199
+ hoverTimeoutRef.current = null;
200
+ }
201
+ setHoveredIndex(index);
202
+ }
203
+ };
204
+ const handleMouseLeave = () => {
205
+ // 약간의 지연을 주어 드롭다운으로 이동할 시간을 줌
206
+ hoverTimeoutRef.current = setTimeout(() => {
207
+ setHoveredIndex(null);
208
+ }, 100);
209
+ };
210
+ const handleDropdownMouseEnter = () => {
211
+ // 드롭다운에 마우스가 들어오면 타이머 취소
212
+ if (hoverTimeoutRef.current) {
213
+ clearTimeout(hoverTimeoutRef.current);
214
+ hoverTimeoutRef.current = null;
215
+ }
216
+ };
217
+ const handleDropdownMouseLeave = () => {
218
+ setHoveredIndex(null);
219
+ };
220
+ // 컴포넌트 언마운트 시 타이머 정리
221
+ useEffect(() => {
222
+ return () => {
223
+ if (hoverTimeoutRef.current) {
224
+ clearTimeout(hoverTimeoutRef.current);
225
+ }
226
+ };
227
+ }, []);
228
+ const activeMenu = hoveredIndex !== null ? contents[hoveredIndex]?.menu : null;
229
+ return (_jsxs(_Fragment, { children: [_jsx("div", { ref: headerRef, className: cn(container), children: _jsxs("div", { className: cn(body), children: [_jsxs("div", { className: cn(leftWrapper), children: [_jsx("div", { onClick: logo.onClick, children: logo.node }), _jsxs("div", { className: cn(essenstialsWrapper), id: "buttonArea", children: [rightButton && rightButton, _jsx("button", { onClick: toggleHeight, className: cn(menuButton), children: _jsx("div", { className: "w-6 h-6", children: _jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", children: [!isExpanded && (_jsx("path", { id: "menu", fill: "#B0B8C1", d: "M4.118 6.2h16a1.2 1.2 0 100-2.4h-16a1.2 1.2 0 100 2.4m16 4.6h-16a1.2 1.2 0 100 2.4h16a1.2 1.2 0 100-2.4m0 7h-16a1.2 1.2 0 100 2.4h16a1.2 1.2 0 100-2.4", "fill-rule": "evenodd" })), isExpanded && (_jsx("path", { id: "close", fill: "#B0B8C1", "fill-rule": "evenodd", d: "M13.815 12l5.651-5.651a1.2 1.2 0 00-1.697-1.698l-5.651 5.652-5.652-5.652a1.201 1.201 0 00-1.697 1.698L10.421 12l-5.652 5.651a1.202 1.202 0 00.849 2.049c.307 0 .614-.117.848-.351l5.652-5.652 5.651 5.652a1.198 1.198 0 001.697 0 1.2 1.2 0 000-1.698L13.815 12z" }))] }) }) })] })] }), _jsxs("div", { ref: menuContentRef, className: cn(rightWrapper), children: [_jsx("div", { className: cn(buttonBoxWrapper), children: contents.map((button, index) => (_jsx("div", { className: "relative", onMouseEnter: () => handleMouseEnter(index), onMouseLeave: handleMouseLeave, children: _jsx("button", { onClick: button.onClick, className: cn(buttonBox), children: button.title }) }, index))) }), _jsx("div", { className: "hidden md:block", children: rightButton && rightButton })] })] }) }), activeMenu && hoveredIndex !== null && isMD && (_jsx("div", { ref: dropdownRef, className: "fixed top-[60px] left-0 w-full bg-white shadow-lg z-50 border-t border-gray-200 hidden md:block", onMouseEnter: handleDropdownMouseEnter, onMouseLeave: handleDropdownMouseLeave, style: { maxWidth: "100vw" }, children: _jsx("div", { className: "max-w-[1200px] mx-auto px-6 md:px-12 py-4", children: _jsxs("div", { className: "flex flex-col md:flex-row gap-4 md:gap-4 lg:gap-6", children: [_jsx("div", { className: "flex-shrink-0 w-full md:w-64 lg:w-80", children: _jsxs("div", { className: "flex flex-col gap-1.5", children: [activeMenu.title.image && (_jsx("div", { className: "mb-0.5", children: _jsx("img", { src: activeMenu.title.image, alt: activeMenu.title.title, className: "w-full object-contain" }) })), _jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx("h3", { className: "text-xl md:text-2xl font-bold text-blue-600", children: activeMenu.title.title }), activeMenu.title.description && (_jsx("p", { className: "text-gray-600 text-xs leading-relaxed", children: activeMenu.title.description }))] }), activeMenu.title.button && (_jsx("div", { className: cn("mt-0.5 flex", activeMenu.title.button.align === "right" ? "justify-end" : "", activeMenu.title.button.align === "center" ? "justify-center" : "", activeMenu.title.button.align === "left" ? "justify-start" : ""), children: _jsx("button", { onClick: () => activeMenu.title.button?.onClick?.(), className: cn(activeMenu.title.button.style ||
230
+ "px-4 py-1.5 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors text-xs font-medium"), children: activeMenu.title.button.title }) }))] }) }), _jsx("div", { className: "flex-1", children: _jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-4 md:gap-x-6 gap-y-3 h-full pt-0 md:pt-4 pl-0 md:pl-4 lg:pl-8", children: activeMenu.content.map((item, itemIndex) => (_jsxs("div", { className: "cursor-pointer group", onClick: () => item.onClick?.(), children: [_jsxs("div", { className: "flex items-center gap-2 mb-1", children: [_jsx("h4", { className: "text-base md:text-lg font-semibold text-gray-900 group-hover:text-blue-600 transition-colors", children: item.title }), item.tag && (_jsx("span", { className: cn("px-2 py-0.5 text-xs font-medium text-white rounded-full whitespace-nowrap", item.tag.background || "bg-blue-600"), children: item.tag.title }))] }), item.description && (_jsx("p", { className: "text-xs text-gray-600 leading-relaxed", children: item.description }))] }, itemIndex))) }) })] }) }) })), isExpanded && !isMD && (_jsxs("div", { ref: mobileMenuRef, className: "fixed inset-0 bg-white z-50 overflow-hidden flex flex-col", onClick: (e) => e.stopPropagation(), children: [_jsxs("div", { className: "bg-white px-4 py-3 flex items-center justify-between", children: [_jsx("div", { className: "flex items-center gap-4", children: _jsx("img", { src: "/images/logos/tosel.png", alt: "Tosel", className: "h-7" }) }), _jsx("button", { onClick: closeMobileMenu, className: "text-black w-6 h-6 flex items-center justify-center", children: _jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "currentColor", className: "w-6 h-6", children: _jsx("path", { d: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" }) }) })] }), _jsx("div", { className: "flex-1 overflow-y-scroll", children: _jsx("div", { className: "border-b border-gray-200", children: contents.map((button, index) => (_jsxs("div", { children: [_jsxs("button", { onClick: (e) => handleMobileMenuClick(index, e), className: "w-full px-4 py-4 flex items-center justify-between border-b border-gray-100 hover:bg-gray-50 transition-colors", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "w-2 h-2 rounded-full bg-green-500" }), _jsx("span", { className: "text-gray-900 font-medium", children: button.title })] }), button.menu && (_jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", className: "w-5 h-5 text-gray-400", children: _jsx("path", { d: "M12 4v16m8-8H4" }) }))] }), button.menu && mobileMenuIndex === index && (_jsxs("div", { className: "bg-gray-50 border-b border-gray-200 animate-in slide-in-from-top-2 duration-200", children: [button.menu.title && (_jsxs("div", { className: "px-4 py-4 border-b border-gray-200", children: [button.menu.title.image && (_jsx("div", { className: "mb-3", children: _jsx("img", { src: button.menu.title.image, alt: button.menu.title.title, className: "w-full max-w-xs h-auto object-contain" }) })), _jsx("h3", { className: "text-xl font-bold text-blue-600 mb-2", children: button.menu.title.title }), button.menu.title.button && (_jsx("div", { className: cn("flex", button.menu.title.button.align === "right"
231
+ ? "justify-end"
232
+ : "", button.menu.title.button.align === "center"
233
+ ? "justify-center"
234
+ : "", button.menu.title.button.align === "left"
235
+ ? "justify-start"
236
+ : ""), children: _jsx("button", { onClick: () => {
237
+ button.menu?.title.button?.onClick?.();
238
+ closeMobileMenu();
239
+ }, className: cn(button.menu.title.button.style ||
240
+ "px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"), children: button.menu.title.button.title }) }))] })), button.menu.content && button.menu.content.length > 0 && (_jsx("div", { className: "px-4 py-2", children: button.menu.content.map((item, itemIndex) => (_jsxs("button", { onClick: () => {
241
+ item.onClick?.();
242
+ closeMobileMenu();
243
+ }, className: "w-full px-4 py-3 flex items-center justify-between border-b border-gray-100 hover:bg-gray-100 transition-colors text-left", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("span", { className: "text-gray-900 font-medium", children: item.title }), item.tag && (_jsx("span", { className: cn("px-2 py-0.5 text-xs font-medium text-white rounded-full", item.tag.background || "bg-green-500"), children: item.tag.title }))] }), _jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", className: "w-5 h-5 text-gray-400", children: _jsx("path", { d: "M12 4v16m8-8H4" }) })] }, itemIndex))) }))] }))] }, index))) }) })] }))] }));
151
244
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edu-tosel/design",
3
- "version": "1.0.349",
3
+ "version": "1.0.351",
4
4
  "description": "UI components for International TOSEL Committee",
5
5
  "keywords": [
6
6
  "jsx",
package/version.txt CHANGED
@@ -1 +1 @@
1
- 1.0.349
1
+ 1.0.351