@shohojdhara/atomix 0.2.8 → 0.3.0
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/CHANGELOG.md +60 -0
- package/README.md +40 -1
- package/dist/atomix.css +96 -39
- package/dist/atomix.min.css +2 -2
- package/dist/index.d.ts +632 -2
- package/dist/index.esm.js +1306 -95
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1330 -94
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/themes/applemix.css +96 -39
- package/dist/themes/applemix.min.css +2 -2
- package/dist/themes/boomdevs.css +96 -39
- package/dist/themes/boomdevs.min.css +2 -2
- package/dist/themes/esrar.css +96 -39
- package/dist/themes/esrar.min.css +2 -2
- package/dist/themes/flashtrade.css +97 -40
- package/dist/themes/flashtrade.min.css +2 -2
- package/dist/themes/mashroom.css +96 -39
- package/dist/themes/mashroom.min.css +3 -3
- package/dist/themes/shaj-default.css +96 -39
- package/dist/themes/shaj-default.min.css +2 -2
- package/package.json +13 -2
- package/src/components/Breadcrumb/Breadcrumb.tsx +8 -3
- package/src/components/Card/Card.tsx +9 -4
- package/src/components/Footer/Footer.stories.tsx +1 -2
- package/src/components/Footer/Footer.tsx +0 -5
- package/src/components/Footer/FooterLink.tsx +3 -2
- package/src/components/Footer/FooterSection.tsx +0 -7
- package/src/components/Navigation/Nav/NavItem.tsx +8 -3
- package/src/components/Navigation/SideMenu/SideMenu.stories.tsx +301 -13
- package/src/components/Navigation/SideMenu/SideMenu.tsx +236 -9
- package/src/components/Navigation/SideMenu/SideMenuItem.tsx +9 -4
- package/src/lib/composables/useSideMenu.ts +89 -30
- package/src/lib/index.ts +5 -0
- package/src/lib/theme/ThemeContext.tsx +17 -0
- package/src/lib/theme/ThemeManager.stories.tsx +472 -0
- package/src/lib/theme/ThemeManager.test.ts +186 -0
- package/src/lib/theme/ThemeManager.ts +501 -0
- package/src/lib/theme/ThemeProvider.tsx +227 -0
- package/src/lib/theme/index.ts +56 -0
- package/src/lib/theme/types.ts +247 -0
- package/src/lib/theme/useTheme.test.tsx +66 -0
- package/src/lib/theme/useTheme.ts +80 -0
- package/src/lib/theme/utils.test.ts +140 -0
- package/src/lib/theme/utils.ts +398 -0
- package/src/lib/types/components.ts +32 -0
- package/src/styles/06-components/_components.card.scss +39 -24
- package/src/styles/06-components/_components.side-menu.scss +79 -18
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import React, { useState, useEffect, useRef, forwardRef } from 'react';
|
|
2
2
|
import { SideMenuProps } from '../../../lib/types/components';
|
|
3
3
|
import { useSideMenu } from '../../../lib/composables/useSideMenu';
|
|
4
|
-
import { SIDE_MENU } from '../../../lib/constants/components';
|
|
5
4
|
import { Icon } from '../../Icon';
|
|
6
5
|
import { AtomixGlass } from '../../AtomixGlass/AtomixGlass';
|
|
7
|
-
import
|
|
6
|
+
import SideMenuList from './SideMenuList';
|
|
7
|
+
import SideMenuItem from './SideMenuItem';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* SideMenu component provides a collapsible navigation menu with title and menu items.
|
|
@@ -26,9 +26,12 @@ export const SideMenu = forwardRef<HTMLDivElement, SideMenuProps>(
|
|
|
26
26
|
{
|
|
27
27
|
title,
|
|
28
28
|
children,
|
|
29
|
+
menuItems = [],
|
|
29
30
|
isOpen,
|
|
30
31
|
onToggle,
|
|
31
32
|
collapsible = true,
|
|
33
|
+
collapsibleDesktop = false,
|
|
34
|
+
defaultCollapsedDesktop = false,
|
|
32
35
|
className = '',
|
|
33
36
|
style,
|
|
34
37
|
disabled = false,
|
|
@@ -42,25 +45,162 @@ export const SideMenu = forwardRef<HTMLDivElement, SideMenuProps>(
|
|
|
42
45
|
isOpenState,
|
|
43
46
|
wrapperRef,
|
|
44
47
|
innerRef,
|
|
48
|
+
sideMenuRef,
|
|
45
49
|
generateSideMenuClass,
|
|
46
50
|
generateWrapperClass,
|
|
47
51
|
handleToggle,
|
|
52
|
+
handleDesktopCollapse,
|
|
48
53
|
} = useSideMenu({
|
|
49
54
|
isOpen,
|
|
50
55
|
onToggle,
|
|
51
56
|
collapsible,
|
|
57
|
+
collapsibleDesktop,
|
|
58
|
+
defaultCollapsedDesktop,
|
|
52
59
|
disabled,
|
|
53
60
|
});
|
|
54
61
|
|
|
55
|
-
const
|
|
62
|
+
const MOBILE_BREAKPOINT = 768;
|
|
63
|
+
|
|
64
|
+
// Track mobile state
|
|
65
|
+
const [isMobileState, setIsMobileState] = useState(() => {
|
|
66
|
+
if (typeof window === 'undefined') return false;
|
|
67
|
+
return window.innerWidth < MOBILE_BREAKPOINT;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Track open state for nested menu items
|
|
71
|
+
const [nestedItemStates, setNestedItemStates] = useState<Record<number, boolean>>(() => {
|
|
72
|
+
const initialState: Record<number, boolean> = {};
|
|
73
|
+
menuItems?.forEach((_, index) => {
|
|
74
|
+
initialState[index] = true; // Default to open
|
|
75
|
+
});
|
|
76
|
+
return initialState;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Refs for nested menu item wrappers
|
|
80
|
+
const nestedWrapperRefs = useRef<Record<number, HTMLDivElement | null>>({});
|
|
81
|
+
const nestedInnerRefs = useRef<Record<number, HTMLDivElement | null>>({});
|
|
82
|
+
const menuItemsLengthRef = useRef<number>(menuItems?.length ?? 0);
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
const handleResize = () => {
|
|
86
|
+
setIsMobileState(window.innerWidth < MOBILE_BREAKPOINT);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
window.addEventListener('resize', handleResize);
|
|
90
|
+
return () => window.removeEventListener('resize', handleResize);
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
// Update nested item states when menuItems change
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
const currentLength = menuItems?.length ?? 0;
|
|
96
|
+
// Only update if the length actually changed to prevent infinite loops
|
|
97
|
+
if (menuItemsLengthRef.current === currentLength) return;
|
|
98
|
+
|
|
99
|
+
menuItemsLengthRef.current = currentLength;
|
|
100
|
+
|
|
101
|
+
setNestedItemStates(prevStates => {
|
|
102
|
+
const newStates: Record<number, boolean> = {};
|
|
103
|
+
menuItems?.forEach((_, index) => {
|
|
104
|
+
newStates[index] = prevStates[index] ?? true;
|
|
105
|
+
});
|
|
106
|
+
return newStates;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Clean up refs for removed items
|
|
110
|
+
Object.keys(nestedWrapperRefs.current).forEach(key => {
|
|
111
|
+
const index = Number(key);
|
|
112
|
+
if (index >= currentLength) {
|
|
113
|
+
delete nestedWrapperRefs.current[index];
|
|
114
|
+
delete nestedInnerRefs.current[index];
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}, [menuItems?.length]);
|
|
118
|
+
|
|
119
|
+
// Set initial heights for nested wrappers on mount and when menuItems change
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (!menuItems?.length) return;
|
|
122
|
+
|
|
123
|
+
const timeoutId = setTimeout(() => {
|
|
124
|
+
menuItems.forEach((_, index) => {
|
|
125
|
+
const wrapper = nestedWrapperRefs.current[index];
|
|
126
|
+
const inner = nestedInnerRefs.current[index];
|
|
127
|
+
const isOpen = nestedItemStates[index] ?? true;
|
|
128
|
+
|
|
129
|
+
if (wrapper && inner) {
|
|
130
|
+
if (isOpen) {
|
|
131
|
+
wrapper.style.height = `${inner.scrollHeight}px`;
|
|
132
|
+
} else {
|
|
133
|
+
wrapper.style.height = '0px';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}, 0);
|
|
138
|
+
|
|
139
|
+
return () => clearTimeout(timeoutId);
|
|
140
|
+
// Only run when menuItems change, nestedItemStates is read but not in deps to avoid loops
|
|
141
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
142
|
+
}, [menuItems?.length]);
|
|
143
|
+
|
|
144
|
+
// Update nested wrapper heights when state changes
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
if (!menuItems?.length) return;
|
|
147
|
+
|
|
148
|
+
const frameIds: number[] = [];
|
|
149
|
+
|
|
150
|
+
Object.keys(nestedItemStates).forEach(key => {
|
|
151
|
+
const index = Number(key);
|
|
152
|
+
const wrapper = nestedWrapperRefs.current[index];
|
|
153
|
+
const inner = nestedInnerRefs.current[index];
|
|
154
|
+
const isOpen = nestedItemStates[index];
|
|
155
|
+
|
|
156
|
+
if (wrapper && inner) {
|
|
157
|
+
const frameId = requestAnimationFrame(() => {
|
|
158
|
+
if (wrapper && inner) {
|
|
159
|
+
if (isOpen) {
|
|
160
|
+
wrapper.style.height = `${inner.scrollHeight}px`;
|
|
161
|
+
} else {
|
|
162
|
+
wrapper.style.height = '0px';
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
frameIds.push(frameId);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return () => {
|
|
171
|
+
frameIds.forEach(id => cancelAnimationFrame(id));
|
|
172
|
+
};
|
|
173
|
+
}, [nestedItemStates, menuItems?.length]);
|
|
174
|
+
|
|
175
|
+
// Combine refs
|
|
176
|
+
const combinedRef = (node: HTMLDivElement | null) => {
|
|
177
|
+
(sideMenuRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
|
178
|
+
if (typeof ref === 'function') {
|
|
179
|
+
ref(node);
|
|
180
|
+
} else if (ref) {
|
|
181
|
+
(ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const sideMenuClass = generateSideMenuClass({
|
|
186
|
+
className,
|
|
187
|
+
isOpen: isOpenState,
|
|
188
|
+
});
|
|
56
189
|
const wrapperClass = generateWrapperClass();
|
|
57
190
|
|
|
58
191
|
// Default toggle icon using Atomix Icon component
|
|
59
192
|
const defaultToggleIcon = <Icon name="CaretRight" size="xs" />;
|
|
60
193
|
|
|
194
|
+
// Determine if we should show toggler (mobile or desktop with collapsibleDesktop)
|
|
195
|
+
const shouldShowToggler =
|
|
196
|
+
(isMobileState && collapsible) || (!isMobileState && collapsibleDesktop);
|
|
197
|
+
// Only show separate title if toggler is NOT shown (toggler already contains the title)
|
|
198
|
+
const shouldShowTitle = title && !shouldShowToggler;
|
|
199
|
+
|
|
61
200
|
const sideMenuContent = (
|
|
62
201
|
<>
|
|
63
|
-
{
|
|
202
|
+
{/* Toggler (works for both mobile and desktop) */}
|
|
203
|
+
{title && shouldShowToggler && (
|
|
64
204
|
<div
|
|
65
205
|
className="c-side-menu__toggler"
|
|
66
206
|
onClick={handleToggle}
|
|
@@ -81,16 +221,98 @@ export const SideMenu = forwardRef<HTMLDivElement, SideMenuProps>(
|
|
|
81
221
|
</div>
|
|
82
222
|
)}
|
|
83
223
|
|
|
84
|
-
{
|
|
224
|
+
{/* Title (non-collapsible) */}
|
|
225
|
+
{shouldShowTitle && <h3 className="c-side-menu__title">{title}</h3>}
|
|
85
226
|
|
|
86
227
|
<div
|
|
87
228
|
ref={wrapperRef}
|
|
88
229
|
className={wrapperClass}
|
|
89
230
|
id={id ? `${id}-content` : undefined}
|
|
90
|
-
aria-hidden={
|
|
231
|
+
aria-hidden={shouldShowToggler ? !isOpenState : false}
|
|
91
232
|
>
|
|
92
233
|
<div ref={innerRef} className="c-side-menu__inner">
|
|
93
|
-
{children}
|
|
234
|
+
{children && children}
|
|
235
|
+
{menuItems?.map((item, index) => {
|
|
236
|
+
const isNestedItemOpen = nestedItemStates[index] ?? true;
|
|
237
|
+
const hasItems = item.items && item.items.length > 0;
|
|
238
|
+
const canToggle = hasItems && !disabled;
|
|
239
|
+
|
|
240
|
+
const handleNestedToggle = () => {
|
|
241
|
+
if (!canToggle) return;
|
|
242
|
+
setNestedItemStates(prev => ({
|
|
243
|
+
...prev,
|
|
244
|
+
[index]: !prev[index],
|
|
245
|
+
}));
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
<div key={index} className="c-side-menu__item">
|
|
250
|
+
{item.title && (
|
|
251
|
+
<div
|
|
252
|
+
className={[
|
|
253
|
+
'c-side-menu__toggler',
|
|
254
|
+
canToggle && 'c-side-menu__toggler--nested',
|
|
255
|
+
isNestedItemOpen && 'is-open',
|
|
256
|
+
]
|
|
257
|
+
.filter(Boolean)
|
|
258
|
+
.join(' ')}
|
|
259
|
+
onClick={canToggle ? handleNestedToggle : undefined}
|
|
260
|
+
role={canToggle ? 'button' : undefined}
|
|
261
|
+
tabIndex={canToggle && !disabled ? 0 : undefined}
|
|
262
|
+
aria-expanded={canToggle ? isNestedItemOpen : undefined}
|
|
263
|
+
aria-disabled={disabled}
|
|
264
|
+
onKeyDown={
|
|
265
|
+
canToggle
|
|
266
|
+
? e => {
|
|
267
|
+
if ((e.key === 'Enter' || e.key === ' ') && !disabled) {
|
|
268
|
+
e.preventDefault();
|
|
269
|
+
handleNestedToggle();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
: undefined
|
|
273
|
+
}
|
|
274
|
+
>
|
|
275
|
+
<span className="c-side-menu__title">{item.title}</span>
|
|
276
|
+
{canToggle && (
|
|
277
|
+
<span className="c-side-menu__toggler-icon">
|
|
278
|
+
{item.toggleIcon || <Icon name="CaretRight" size="xs" />}
|
|
279
|
+
</span>
|
|
280
|
+
)}
|
|
281
|
+
</div>
|
|
282
|
+
)}
|
|
283
|
+
{hasItems && (
|
|
284
|
+
<div
|
|
285
|
+
ref={node => {
|
|
286
|
+
nestedWrapperRefs.current[index] = node;
|
|
287
|
+
}}
|
|
288
|
+
className="c-side-menu__nested-wrapper"
|
|
289
|
+
>
|
|
290
|
+
<div
|
|
291
|
+
ref={node => {
|
|
292
|
+
nestedInnerRefs.current[index] = node;
|
|
293
|
+
}}
|
|
294
|
+
className="c-side-menu__nested-inner"
|
|
295
|
+
>
|
|
296
|
+
<SideMenuList>
|
|
297
|
+
{item.items?.map((subItem, subIndex) => (
|
|
298
|
+
<SideMenuItem
|
|
299
|
+
key={subIndex}
|
|
300
|
+
href={subItem.href}
|
|
301
|
+
onClick={subItem.onClick}
|
|
302
|
+
active={subItem.active}
|
|
303
|
+
disabled={subItem.disabled}
|
|
304
|
+
icon={subItem.icon}
|
|
305
|
+
>
|
|
306
|
+
{subItem.title}
|
|
307
|
+
</SideMenuItem>
|
|
308
|
+
))}
|
|
309
|
+
</SideMenuList>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
)}
|
|
313
|
+
</div>
|
|
314
|
+
);
|
|
315
|
+
})}
|
|
94
316
|
</div>
|
|
95
317
|
</div>
|
|
96
318
|
</>
|
|
@@ -106,7 +328,12 @@ export const SideMenu = forwardRef<HTMLDivElement, SideMenuProps>(
|
|
|
106
328
|
const glassProps = glass === true ? defaultGlassProps : { ...defaultGlassProps, ...glass };
|
|
107
329
|
return (
|
|
108
330
|
<AtomixGlass {...glassProps}>
|
|
109
|
-
<div
|
|
331
|
+
<div
|
|
332
|
+
ref={combinedRef}
|
|
333
|
+
className={sideMenuClass + ' c-side-menu--glass'}
|
|
334
|
+
id={id}
|
|
335
|
+
style={style}
|
|
336
|
+
>
|
|
110
337
|
{sideMenuContent}
|
|
111
338
|
</div>
|
|
112
339
|
</AtomixGlass>
|
|
@@ -114,7 +341,7 @@ export const SideMenu = forwardRef<HTMLDivElement, SideMenuProps>(
|
|
|
114
341
|
}
|
|
115
342
|
|
|
116
343
|
return (
|
|
117
|
-
<div ref={
|
|
344
|
+
<div ref={combinedRef} className={sideMenuClass} id={id} style={style}>
|
|
118
345
|
{sideMenuContent}
|
|
119
346
|
</div>
|
|
120
347
|
);
|
|
@@ -66,10 +66,15 @@ export const SideMenuItem = forwardRef<HTMLAnchorElement | HTMLButtonElement, Si
|
|
|
66
66
|
// Render as link if href is provided
|
|
67
67
|
if (href) {
|
|
68
68
|
return LinkComponent ? (
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
(() => {
|
|
70
|
+
const Component = LinkComponent as React.ComponentType<any>;
|
|
71
|
+
return (
|
|
72
|
+
<Component {...linkProps}>
|
|
73
|
+
{icon && <span className="c-side-menu__link-icon">{icon}</span>}
|
|
74
|
+
<span className="c-side-menu__link-text">{children}</span>
|
|
75
|
+
</Component>
|
|
76
|
+
);
|
|
77
|
+
})()
|
|
73
78
|
) : (
|
|
74
79
|
<a {...linkProps}>
|
|
75
80
|
{icon && <span className="c-side-menu__link-icon">{icon}</span>}
|
|
@@ -11,76 +11,126 @@ export function useSideMenu(initialProps?: Partial<SideMenuProps>) {
|
|
|
11
11
|
// Default side menu properties
|
|
12
12
|
const defaultProps: Partial<SideMenuProps> = {
|
|
13
13
|
collapsible: true,
|
|
14
|
+
collapsibleDesktop: false,
|
|
15
|
+
defaultCollapsedDesktop: false,
|
|
14
16
|
isOpen: false,
|
|
15
17
|
...initialProps,
|
|
16
18
|
};
|
|
17
19
|
|
|
18
20
|
// Local open state for when not controlled externally
|
|
19
|
-
const [isOpenState, setIsOpenState] = useState(
|
|
21
|
+
const [isOpenState, setIsOpenState] = useState(
|
|
22
|
+
defaultProps.defaultCollapsedDesktop !== undefined
|
|
23
|
+
? !defaultProps.defaultCollapsedDesktop
|
|
24
|
+
: (defaultProps.isOpen || false)
|
|
25
|
+
);
|
|
20
26
|
|
|
21
27
|
// Refs for managing responsive behavior
|
|
22
28
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
23
29
|
const innerRef = useRef<HTMLDivElement>(null);
|
|
30
|
+
const sideMenuRef = useRef<HTMLDivElement>(null);
|
|
24
31
|
|
|
25
32
|
// Update local state when external state changes
|
|
26
33
|
useEffect(() => {
|
|
27
34
|
if (typeof defaultProps.isOpen !== 'undefined') {
|
|
28
35
|
setIsOpenState(defaultProps.isOpen);
|
|
36
|
+
} else if (defaultProps.defaultCollapsedDesktop !== undefined) {
|
|
37
|
+
setIsOpenState(!defaultProps.defaultCollapsedDesktop);
|
|
29
38
|
}
|
|
30
|
-
}, [defaultProps.isOpen]);
|
|
39
|
+
}, [defaultProps.isOpen, defaultProps.defaultCollapsedDesktop]);
|
|
31
40
|
|
|
32
|
-
//
|
|
41
|
+
// Set initial height on mount
|
|
33
42
|
useEffect(() => {
|
|
34
|
-
const
|
|
35
|
-
|
|
43
|
+
const isMobile = window.innerWidth < 768;
|
|
44
|
+
const shouldCollapse = isMobile ? defaultProps.collapsible : defaultProps.collapsibleDesktop;
|
|
45
|
+
const currentOpen = typeof defaultProps.isOpen !== 'undefined' ? defaultProps.isOpen : isOpenState;
|
|
36
46
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
47
|
+
if (shouldCollapse && wrapperRef.current && innerRef.current) {
|
|
48
|
+
// Use setTimeout to ensure DOM is fully rendered
|
|
49
|
+
const timeoutId = setTimeout(() => {
|
|
50
|
+
if (wrapperRef.current && innerRef.current) {
|
|
51
|
+
if (currentOpen) {
|
|
52
|
+
wrapperRef.current.style.height = `${innerRef.current.scrollHeight}px`;
|
|
53
|
+
} else {
|
|
54
|
+
wrapperRef.current.style.height = '0px';
|
|
55
|
+
}
|
|
43
56
|
}
|
|
57
|
+
}, 0);
|
|
58
|
+
|
|
59
|
+
return () => clearTimeout(timeoutId);
|
|
60
|
+
} else if (!shouldCollapse && wrapperRef.current) {
|
|
61
|
+
wrapperRef.current.style.height = 'auto';
|
|
62
|
+
}
|
|
63
|
+
}, []); // Only run on mount
|
|
64
|
+
|
|
65
|
+
// Handle responsive behavior - vertical collapse for both mobile and desktop
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
const handleResize = () => {
|
|
68
|
+
const isMobile = window.innerWidth < 768; // MD breakpoint
|
|
69
|
+
const shouldCollapse = isMobile ? defaultProps.collapsible : defaultProps.collapsibleDesktop;
|
|
44
70
|
|
|
45
|
-
|
|
71
|
+
if (!shouldCollapse) {
|
|
72
|
+
// Not collapsible - always show content
|
|
46
73
|
if (wrapperRef.current) {
|
|
47
74
|
wrapperRef.current.style.height = 'auto';
|
|
48
75
|
}
|
|
49
|
-
} else if (
|
|
50
|
-
// Set proper height for mobile
|
|
76
|
+
} else if (wrapperRef.current && innerRef.current) {
|
|
77
|
+
// Set proper height for vertical animation (both mobile and desktop)
|
|
51
78
|
const currentOpen =
|
|
52
79
|
typeof defaultProps.isOpen !== 'undefined' ? defaultProps.isOpen : isOpenState;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
wrapperRef.current.
|
|
57
|
-
|
|
80
|
+
|
|
81
|
+
// Use requestAnimationFrame to ensure DOM is ready
|
|
82
|
+
requestAnimationFrame(() => {
|
|
83
|
+
if (wrapperRef.current && innerRef.current) {
|
|
84
|
+
if (currentOpen) {
|
|
85
|
+
wrapperRef.current.style.height = `${innerRef.current.scrollHeight}px`;
|
|
86
|
+
} else {
|
|
87
|
+
wrapperRef.current.style.height = '0px';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
58
91
|
}
|
|
59
92
|
};
|
|
60
93
|
|
|
61
|
-
|
|
94
|
+
// Initial call with a small delay to ensure DOM is ready
|
|
95
|
+
const timeoutId = setTimeout(handleResize, 0);
|
|
62
96
|
window.addEventListener('resize', handleResize);
|
|
63
97
|
|
|
64
98
|
return () => {
|
|
99
|
+
clearTimeout(timeoutId);
|
|
65
100
|
window.removeEventListener('resize', handleResize);
|
|
66
101
|
};
|
|
67
|
-
}, [
|
|
102
|
+
}, [
|
|
103
|
+
defaultProps.collapsible,
|
|
104
|
+
defaultProps.collapsibleDesktop,
|
|
105
|
+
defaultProps.isOpen,
|
|
106
|
+
defaultProps.onToggle,
|
|
107
|
+
isOpenState,
|
|
108
|
+
]);
|
|
68
109
|
|
|
69
|
-
// Update wrapper height when open state changes
|
|
110
|
+
// Update wrapper height when open state changes (both mobile and desktop)
|
|
70
111
|
useEffect(() => {
|
|
71
112
|
const isMobile = window.innerWidth < 768;
|
|
113
|
+
const shouldCollapse = isMobile ? defaultProps.collapsible : defaultProps.collapsibleDesktop;
|
|
72
114
|
|
|
73
|
-
if (
|
|
115
|
+
if (shouldCollapse && wrapperRef.current && innerRef.current) {
|
|
74
116
|
const currentOpen =
|
|
75
117
|
typeof defaultProps.isOpen !== 'undefined' ? defaultProps.isOpen : isOpenState;
|
|
76
118
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
119
|
+
// Use requestAnimationFrame to ensure DOM is ready
|
|
120
|
+
requestAnimationFrame(() => {
|
|
121
|
+
if (wrapperRef.current && innerRef.current) {
|
|
122
|
+
if (currentOpen) {
|
|
123
|
+
wrapperRef.current.style.height = `${innerRef.current.scrollHeight}px`;
|
|
124
|
+
} else {
|
|
125
|
+
wrapperRef.current.style.height = '0px';
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
} else if (!shouldCollapse && wrapperRef.current) {
|
|
130
|
+
// Not collapsible - always show content
|
|
131
|
+
wrapperRef.current.style.height = 'auto';
|
|
82
132
|
}
|
|
83
|
-
}, [defaultProps.isOpen, isOpenState, defaultProps.collapsible]);
|
|
133
|
+
}, [defaultProps.isOpen, isOpenState, defaultProps.collapsible, defaultProps.collapsibleDesktop]);
|
|
84
134
|
|
|
85
135
|
/**
|
|
86
136
|
* Generate side menu class based on properties
|
|
@@ -104,7 +154,7 @@ export function useSideMenu(initialProps?: Partial<SideMenuProps>) {
|
|
|
104
154
|
};
|
|
105
155
|
|
|
106
156
|
/**
|
|
107
|
-
* Handle toggle click
|
|
157
|
+
* Handle toggle click (mobile)
|
|
108
158
|
*/
|
|
109
159
|
const handleToggle = () => {
|
|
110
160
|
if (defaultProps.disabled) return;
|
|
@@ -121,6 +171,13 @@ export function useSideMenu(initialProps?: Partial<SideMenuProps>) {
|
|
|
121
171
|
}
|
|
122
172
|
};
|
|
123
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Handle desktop collapse toggle (uses same toggle as mobile)
|
|
176
|
+
*/
|
|
177
|
+
const handleDesktopCollapse = () => {
|
|
178
|
+
handleToggle();
|
|
179
|
+
};
|
|
180
|
+
|
|
124
181
|
/**
|
|
125
182
|
* Get current open state
|
|
126
183
|
* @returns Current open state
|
|
@@ -134,9 +191,11 @@ export function useSideMenu(initialProps?: Partial<SideMenuProps>) {
|
|
|
134
191
|
isOpenState: getCurrentOpenState(),
|
|
135
192
|
wrapperRef,
|
|
136
193
|
innerRef,
|
|
194
|
+
sideMenuRef,
|
|
137
195
|
generateSideMenuClass,
|
|
138
196
|
generateWrapperClass,
|
|
139
197
|
handleToggle,
|
|
198
|
+
handleDesktopCollapse,
|
|
140
199
|
getCurrentOpenState,
|
|
141
200
|
};
|
|
142
201
|
}
|
package/src/lib/index.ts
CHANGED
|
@@ -3,9 +3,14 @@ import * as composablesImport from './composables';
|
|
|
3
3
|
import * as utilsImport from './utils';
|
|
4
4
|
import * as typesImport from './types';
|
|
5
5
|
import * as constantsImport from './constants';
|
|
6
|
+
import * as themeImport from './theme';
|
|
6
7
|
|
|
7
8
|
// Export as namespaces with explicit typing
|
|
8
9
|
export const composables: typeof composablesImport = composablesImport;
|
|
9
10
|
export const utils: typeof utilsImport = utilsImport;
|
|
10
11
|
export const types: typeof typesImport = typesImport;
|
|
11
12
|
export const constants: typeof constantsImport = constantsImport;
|
|
13
|
+
export const theme: typeof themeImport = themeImport;
|
|
14
|
+
|
|
15
|
+
// Also export theme utilities directly for convenience
|
|
16
|
+
export * from './theme';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme Context
|
|
3
|
+
*
|
|
4
|
+
* React context for theme management
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createContext } from 'react';
|
|
8
|
+
import type { ThemeContextValue } from './types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Theme context with default values
|
|
12
|
+
*/
|
|
13
|
+
export const ThemeContext = createContext<ThemeContextValue | null>(null);
|
|
14
|
+
|
|
15
|
+
ThemeContext.displayName = 'ThemeContext';
|
|
16
|
+
|
|
17
|
+
export default ThemeContext;
|