@shohojdhara/atomix 0.3.0 → 0.3.1

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.
Files changed (95) hide show
  1. package/dist/atomix.css +309 -105
  2. package/dist/atomix.min.css +3 -5
  3. package/dist/index.d.ts +804 -53
  4. package/dist/index.esm.js +16367 -16413
  5. package/dist/index.esm.js.map +1 -1
  6. package/dist/index.js +16275 -16336
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.min.js +1 -1
  9. package/dist/index.min.js.map +1 -1
  10. package/dist/themes/applemix.css +309 -105
  11. package/dist/themes/applemix.min.css +5 -7
  12. package/dist/themes/boomdevs.css +202 -10
  13. package/dist/themes/boomdevs.min.css +3 -5
  14. package/dist/themes/esrar.css +309 -105
  15. package/dist/themes/esrar.min.css +4 -6
  16. package/dist/themes/flashtrade.css +310 -105
  17. package/dist/themes/flashtrade.min.css +5 -7
  18. package/dist/themes/mashroom.css +300 -96
  19. package/dist/themes/mashroom.min.css +4 -6
  20. package/dist/themes/shaj-default.css +300 -96
  21. package/dist/themes/shaj-default.min.css +4 -6
  22. package/package.json +1 -1
  23. package/src/components/AtomixGlass/AtomixGlass.test.tsx +21 -32
  24. package/src/components/AtomixGlass/AtomixGlass.tsx +55 -42
  25. package/src/components/AtomixGlass/AtomixGlassContainer.tsx +205 -57
  26. package/src/components/AtomixGlass/GlassFilter.tsx +22 -8
  27. package/src/components/AtomixGlass/__snapshots__/AtomixGlass.test.tsx.snap +221 -0
  28. package/src/components/AtomixGlass/atomixGLass.old.tsx +0 -3
  29. package/src/components/AtomixGlass/shader-utils.ts +8 -0
  30. package/src/components/AtomixGlass/stories/AtomixGlass.stories.tsx +319 -100
  31. package/src/components/AtomixGlass/stories/Examples.stories.tsx +601 -105
  32. package/src/components/AtomixGlass/stories/Modes.stories.tsx +30 -12
  33. package/src/components/AtomixGlass/stories/Playground.stories.tsx +173 -38
  34. package/src/components/AtomixGlass/stories/ShaderVariants.stories.tsx +18 -18
  35. package/src/components/AtomixGlass/stories/shared-components.tsx +27 -5
  36. package/src/components/Button/Button.tsx +62 -17
  37. package/src/components/Callout/Callout.test.tsx +8 -14
  38. package/src/components/Card/Card.tsx +103 -1
  39. package/src/components/Card/index.ts +3 -2
  40. package/src/components/Icon/index.ts +1 -1
  41. package/src/components/Modal/Modal.stories.tsx +29 -38
  42. package/src/components/Modal/Modal.tsx +4 -4
  43. package/src/components/Navigation/SideMenu/SideMenu.tsx +49 -41
  44. package/src/components/Navigation/SideMenu/SideMenuItem.tsx +63 -24
  45. package/src/components/Popover/Popover.tsx +1 -1
  46. package/src/components/VideoPlayer/VideoPlayer.stories.tsx +977 -400
  47. package/src/components/VideoPlayer/VideoPlayer.tsx +1 -6
  48. package/src/lib/composables/shared-mouse-tracker.ts +133 -0
  49. package/src/lib/composables/useAtomixGlass.ts +303 -115
  50. package/src/lib/theme/ThemeManager.integration.test.ts +124 -0
  51. package/src/lib/theme/ThemeManager.stories.tsx +13 -13
  52. package/src/lib/theme/ThemeManager.test.ts +4 -0
  53. package/src/lib/theme/ThemeManager.ts +203 -59
  54. package/src/lib/theme/ThemeProvider.tsx +183 -33
  55. package/src/lib/theme/composeTheme.ts +375 -0
  56. package/src/lib/theme/createTheme.test.ts +475 -0
  57. package/src/lib/theme/createTheme.ts +510 -0
  58. package/src/lib/theme/generateCSSVariables.ts +713 -0
  59. package/src/lib/theme/index.ts +67 -0
  60. package/src/lib/theme/themeUtils.ts +333 -0
  61. package/src/lib/theme/types.ts +337 -8
  62. package/src/lib/theme/useTheme.test.tsx +2 -1
  63. package/src/lib/theme/useTheme.ts +6 -22
  64. package/src/lib/types/components.ts +148 -59
  65. package/src/styles/01-settings/_index.scss +2 -2
  66. package/src/styles/01-settings/_settings.badge.scss +2 -2
  67. package/src/styles/01-settings/_settings.border-radius.scss +1 -1
  68. package/src/styles/01-settings/{_settings.maps.scss → _settings.design-tokens.scss} +163 -49
  69. package/src/styles/01-settings/_settings.modal.scss +1 -1
  70. package/src/styles/01-settings/_settings.spacing.scss +14 -13
  71. package/src/styles/03-generic/_generic.root.scss +131 -50
  72. package/src/styles/05-objects/_objects.block.scss +1 -1
  73. package/src/styles/06-components/_components.atomix-glass.scss +20 -22
  74. package/src/styles/06-components/_components.badge.scss +2 -2
  75. package/src/styles/06-components/_components.button.scss +1 -1
  76. package/src/styles/06-components/_components.callout.scss +1 -1
  77. package/src/styles/06-components/_components.card.scss +74 -2
  78. package/src/styles/06-components/_components.chart.scss +1 -1
  79. package/src/styles/06-components/_components.dropdown.scss +6 -0
  80. package/src/styles/06-components/_components.footer.scss +1 -1
  81. package/src/styles/06-components/_components.list-group.scss +1 -1
  82. package/src/styles/06-components/_components.list.scss +1 -1
  83. package/src/styles/06-components/_components.menu.scss +1 -1
  84. package/src/styles/06-components/_components.messages.scss +1 -1
  85. package/src/styles/06-components/_components.modal.scss +7 -2
  86. package/src/styles/06-components/_components.navbar.scss +1 -1
  87. package/src/styles/06-components/_components.popover.scss +10 -0
  88. package/src/styles/06-components/_components.product-review.scss +1 -1
  89. package/src/styles/06-components/_components.progress.scss +1 -1
  90. package/src/styles/06-components/_components.rating.scss +1 -1
  91. package/src/styles/06-components/_components.spinner.scss +1 -1
  92. package/src/styles/99-utilities/_utilities.background.scss +1 -1
  93. package/src/styles/99-utilities/_utilities.border.scss +1 -1
  94. package/src/styles/99-utilities/_utilities.link.scss +1 -1
  95. package/src/styles/99-utilities/_utilities.text.scss +1 -1
@@ -151,13 +151,9 @@ describe('Callout Component', () => {
151
151
  const glassProps = JSON.parse(glassElement.getAttribute('data-glass-props') || '{}');
152
152
 
153
153
  expect(glassProps).toMatchObject({
154
- displacementScale: 40,
155
- blurAmount: 0,
156
- saturation: 160,
157
- aberrationIntensity: 1,
154
+ displacementScale: 30,
158
155
  cornerRadius: 8,
159
- overLight: false,
160
- mode: 'standard',
156
+ elasticity: 0,
161
157
  });
162
158
  });
163
159
 
@@ -184,10 +180,8 @@ describe('Callout Component', () => {
184
180
  blurAmount: 2,
185
181
  saturation: 180,
186
182
  cornerRadius: 12,
187
- // Default values that weren't overridden
188
- aberrationIntensity: 1,
189
- overLight: false,
190
- mode: 'standard',
183
+ // Default values from Callout
184
+ elasticity: 0,
191
185
  });
192
186
  });
193
187
 
@@ -196,7 +190,7 @@ describe('Callout Component', () => {
196
190
  const TestIcon = () => <div data-testid="test-icon">Icon</div>;
197
191
  const actions = <button data-testid="action-button">Action</button>;
198
192
 
199
- render(
193
+ const { container } = render(
200
194
  <Callout
201
195
  title="Glass Test"
202
196
  variant="success"
@@ -223,9 +217,9 @@ describe('Callout Component', () => {
223
217
  // Check that glass wrapper is present
224
218
  expect(screen.getByTestId('atomix-glass')).toBeInTheDocument();
225
219
 
226
- // Check that all classes are applied
227
- const calloutElement = screen.getByTestId('atomix-glass').firstChild;
228
- expect(calloutElement).toHaveClass(
220
+ // Check that all classes are applied to the outer wrapper
221
+ const outerCallout = container.querySelector('.c-callout');
222
+ expect(outerCallout).toHaveClass(
229
223
  'c-callout',
230
224
  'c-callout--success',
231
225
  'c-callout--oneline',
@@ -12,6 +12,8 @@ export const Card = React.memo(
12
12
  variant = '',
13
13
  appearance = 'filled',
14
14
  elevation = 'none',
15
+ hoverable = false,
16
+ hoverElevation = 'md',
15
17
  // Layout
16
18
  row = false,
17
19
  flat = false,
@@ -77,6 +79,9 @@ export const Card = React.memo(
77
79
  elevation === 'md' ? CARD.CLASSES.ELEVATION_MD : '',
78
80
  elevation === 'lg' ? CARD.CLASSES.ELEVATION_LG : '',
79
81
  elevation === 'xl' ? CARD.CLASSES.ELEVATION_XL : '',
82
+ // Hoverable modifier
83
+ hoverable ? 'c-card--hoverable' : '',
84
+ hoverable && hoverElevation ? `c-card--hover-elevation-${hoverElevation}` : '',
80
85
  // Layout modifiers
81
86
  row ? CARD.CLASSES.ROW : '',
82
87
  flat ? CARD.CLASSES.FLAT : '',
@@ -91,7 +96,7 @@ export const Card = React.memo(
91
96
  ]
92
97
  .filter(Boolean)
93
98
  .join(' '),
94
- [size, variant, appearance, elevation, row, flat, active, disabled, loading, selected, interactive, isClickable, glass, className]
99
+ [size, variant, appearance, elevation, hoverable, hoverElevation, row, flat, active, disabled, loading, selected, interactive, isClickable, glass, className]
95
100
  );
96
101
 
97
102
  // Determine ARIA role
@@ -267,6 +272,103 @@ export const Card = React.memo(
267
272
 
268
273
  Card.displayName = 'Card';
269
274
 
275
+ // Card subcomponents for structured content
276
+ export interface CardHeaderProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {
277
+ /**
278
+ * Header title
279
+ */
280
+ title?: React.ReactNode;
281
+ /**
282
+ * Header subtitle
283
+ */
284
+ subtitle?: React.ReactNode;
285
+ /**
286
+ * Action element (e.g., button) to display in header
287
+ */
288
+ action?: React.ReactNode;
289
+ /**
290
+ * Icon to display in header
291
+ */
292
+ icon?: React.ReactNode;
293
+ }
294
+
295
+ export const CardHeader = forwardRef<HTMLDivElement, CardHeaderProps>(
296
+ ({ title, subtitle, action, icon, children, className = '', ...props }, ref) => {
297
+ const headerClasses = `${CARD.SELECTORS.HEADER.substring(1)} ${className}`.trim();
298
+
299
+ return (
300
+ <div ref={ref} className={headerClasses} {...props}>
301
+ {icon && <div className={CARD.SELECTORS.ICON.substring(1)}>{icon}</div>}
302
+ {(title || subtitle) && (
303
+ <div>
304
+ {title && <h3 className={CARD.SELECTORS.TITLE.substring(1)}>{title}</h3>}
305
+ {subtitle && <p className={CARD.SELECTORS.TEXT.substring(1)}>{subtitle}</p>}
306
+ </div>
307
+ )}
308
+ {action && <div className={CARD.SELECTORS.ACTIONS.substring(1)}>{action}</div>}
309
+ {children}
310
+ </div>
311
+ );
312
+ }
313
+ );
314
+
315
+ CardHeader.displayName = 'CardHeader';
316
+
317
+ export interface CardBodyProps extends React.HTMLAttributes<HTMLDivElement> {
318
+ /**
319
+ * Make body scrollable
320
+ */
321
+ scrollable?: boolean;
322
+ /**
323
+ * Maximum height for scrollable body
324
+ */
325
+ maxHeight?: string | number;
326
+ }
327
+
328
+ export const CardBody = forwardRef<HTMLDivElement, CardBodyProps>(
329
+ ({ scrollable = false, maxHeight, children, className = '', style, ...props }, ref) => {
330
+ const bodyClasses = `${CARD.SELECTORS.BODY.substring(1)} ${scrollable ? 'c-card__body--scrollable' : ''} ${className}`.trim();
331
+ const bodyStyle: React.CSSProperties = {
332
+ ...style,
333
+ ...(scrollable && maxHeight ? { maxHeight: typeof maxHeight === 'number' ? `${maxHeight}px` : maxHeight, overflowY: 'auto' } : {}),
334
+ };
335
+
336
+ return (
337
+ <div ref={ref} className={bodyClasses} style={bodyStyle} {...props}>
338
+ {children}
339
+ </div>
340
+ );
341
+ }
342
+ );
343
+
344
+ CardBody.displayName = 'CardBody';
345
+
346
+ export interface CardFooterProps extends React.HTMLAttributes<HTMLDivElement> {
347
+ /**
348
+ * Footer alignment
349
+ */
350
+ align?: 'start' | 'center' | 'end' | 'between';
351
+ }
352
+
353
+ export const CardFooter = forwardRef<HTMLDivElement, CardFooterProps>(
354
+ ({ align, children, className = '', style, ...props }, ref) => {
355
+ const footerClasses = `${CARD.SELECTORS.FOOTER.substring(1)} ${align ? `c-card__footer--align-${align}` : ''} ${className}`.trim();
356
+
357
+ return (
358
+ <div ref={ref} className={footerClasses} style={style} {...props}>
359
+ {children}
360
+ </div>
361
+ );
362
+ }
363
+ );
364
+
365
+ CardFooter.displayName = 'CardFooter';
366
+
367
+ // Attach subcomponents to Card
368
+ (Card as any).Header = CardHeader;
369
+ (Card as any).Body = CardBody;
370
+ (Card as any).Footer = CardFooter;
371
+
270
372
  export type { CardProps };
271
373
 
272
374
  export default Card;
@@ -5,8 +5,9 @@
5
5
  * Types and hooks are defined in the lib directory.
6
6
  */
7
7
 
8
- // Export the main Card component
9
- export { default as Card } from './Card';
8
+ // Export the main Card component with subcomponents
9
+ export { default as Card, CardHeader, CardBody, CardFooter } from './Card';
10
+ export type { CardHeaderProps, CardBodyProps, CardFooterProps } from './Card';
10
11
 
11
12
  // Export the ElevationCard variant
12
13
  export { default as ElevationCard } from './ElevationCard';
@@ -1,2 +1,2 @@
1
- export { Icon, type IconProps, type IconSize, type IconWeight } from './Icon';
1
+ export { Icon, type IconProps, type IconSize, type IconWeight, type PhosphorIconsType } from './Icon';
2
2
  export { default } from './Icon';
@@ -317,6 +317,7 @@ export const GlassModal: Story = {
317
317
  appearance. The glass effect creates a modern, elegant look that works well over
318
318
  colorful backgrounds.
319
319
  </p>
320
+ <img src="https://picsum.photos/800/410" alt="desert" style={{ maxWidth: '100%' }} />
320
321
  <p>
321
322
  The glass effect includes displacement, blur, and chromatic aberration for a premium
322
323
  feel.
@@ -329,9 +330,14 @@ export const GlassModal: Story = {
329
330
  Story => (
330
331
  <div
331
332
  style={{
332
- background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
333
- minHeight: '100vh',
334
- padding: '2rem',
333
+ background: 'url(https://picsum.photos/1920/1080)',
334
+ height: '100vh',
335
+ width: '100vw',
336
+ backgroundSize: 'cover',
337
+ backgroundPosition: 'center',
338
+ display: 'flex',
339
+ alignItems: 'center',
340
+ justifyContent: 'center',
335
341
  }}
336
342
  >
337
343
  <Story />
@@ -375,23 +381,10 @@ export const GlassModalCustom: Story = {
375
381
  }
376
382
  footer={
377
383
  <>
378
- <div
379
- className="c-btn c-btn--outline-secondary"
380
- onClick={() => setIsOpen(false)}
381
- style={{
382
- cursor: 'pointer',
383
- padding: '8px 16px',
384
- display: 'inline-block',
385
- marginRight: '8px',
386
- }}
387
- >
384
+ <div className="c-btn c-btn--outline-secondary" onClick={() => setIsOpen(false)}>
388
385
  Cancel
389
386
  </div>
390
- <div
391
- className="c-btn c-btn--primary"
392
- onClick={() => setIsOpen(false)}
393
- style={{ cursor: 'pointer', padding: '8px 16px', display: 'inline-block' }}
394
- >
387
+ <div className="c-btn c-btn--primary" onClick={() => setIsOpen(false)}>
395
388
  Confirm
396
389
  </div>
397
390
  </>
@@ -402,6 +395,7 @@ export const GlassModalCustom: Story = {
402
395
  aberration. The polar mode creates a different visual effect compared to the standard
403
396
  shader mode.
404
397
  </p>
398
+ <img src="https://picsum.photos/800/410" alt="desert" style={{ maxWidth: '100%' }} />
405
399
  </Modal>
406
400
  </>
407
401
  );
@@ -410,12 +404,14 @@ export const GlassModalCustom: Story = {
410
404
  Story => (
411
405
  <div
412
406
  style={{
413
- background:
414
- 'url(https://images.unsplash.com/photo-1744872665943-fd335d371059?q=80&w=3024&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D)',
407
+ background: 'url(https://picsum.photos/1920/1080)',
415
408
  height: '100vh',
416
409
  width: '100vw',
417
410
  backgroundSize: 'cover',
418
411
  backgroundPosition: 'center',
412
+ display: 'flex',
413
+ alignItems: 'center',
414
+ justifyContent: 'center',
419
415
  }}
420
416
  >
421
417
  <Story />
@@ -489,23 +485,10 @@ export const GlassModalSizes: Story = {
489
485
  glass={true}
490
486
  footer={
491
487
  <>
492
- <div
493
- className="c-btn c-btn--outline-secondary"
494
- onClick={() => setIsOpen(false)}
495
- style={{
496
- cursor: 'pointer',
497
- padding: '8px 16px',
498
- display: 'inline-block',
499
- marginRight: '8px',
500
- }}
501
- >
488
+ <div className="c-btn c-btn--outline-secondary" onClick={() => setIsOpen(false)}>
502
489
  Cancel
503
490
  </div>
504
- <div
505
- className="c-btn c-btn--primary"
506
- onClick={() => setIsOpen(false)}
507
- style={{ cursor: 'pointer', padding: '8px 16px', display: 'inline-block' }}
508
- >
491
+ <div className="c-btn c-btn--primary" onClick={() => setIsOpen(false)}>
509
492
  Confirm
510
493
  </div>
511
494
  </>
@@ -515,6 +498,9 @@ export const GlassModalSizes: Story = {
515
498
  <p>
516
499
  The glass effect adapts to different modal sizes while maintaining its visual appeal.
517
500
  </p>
501
+ <p>
502
+ The glass effect enhances the modal's appearance, making it visually appealing and easier to read.
503
+ </p>
518
504
  </Modal>
519
505
  </div>
520
506
  );
@@ -523,9 +509,14 @@ export const GlassModalSizes: Story = {
523
509
  Story => (
524
510
  <div
525
511
  style={{
526
- background: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
527
- minHeight: '100vh',
528
- padding: '2rem',
512
+ background: 'url(https://picsum.photos/1920/1080)',
513
+ height: '100vh',
514
+ width: '100vw',
515
+ backgroundSize: 'cover',
516
+ backgroundPosition: 'center',
517
+ display: 'flex',
518
+ alignItems: 'center',
519
+ justifyContent: 'center',
529
520
  }}
530
521
  >
531
522
  <Story />
@@ -197,11 +197,11 @@ export const Modal: React.FC<ModalProps> = ({
197
197
  ? // Default glass settings for modals
198
198
  (() => {
199
199
  const defaultGlassProps = {
200
- displacementScale: 100,
201
- blurAmount: 2,
202
- aberrationIntensity: 1,
203
- cornerRadius: 12,
200
+ displacementScale: document.querySelector('.c-modal---glass .c-modal__content')?.clientHeight,
201
+ blurAmount: 2.2,
202
+ elasticity: 0,
204
203
  mode: 'shader' as const,
204
+ shaderMode: 'premiumGlass'
205
205
  };
206
206
 
207
207
  const glassProps =
@@ -1,11 +1,32 @@
1
- import React, { useState, useEffect, useRef, forwardRef } from 'react';
1
+ import React, { useState, useEffect, useRef, forwardRef, createContext, useContext } from 'react';
2
2
  import { SideMenuProps } from '../../../lib/types/components';
3
3
  import { useSideMenu } from '../../../lib/composables/useSideMenu';
4
4
  import { Icon } from '../../Icon';
5
5
  import { AtomixGlass } from '../../AtomixGlass/AtomixGlass';
6
+ import useForkRef from '../../../lib/utils/useForkRef';
6
7
  import SideMenuList from './SideMenuList';
7
8
  import SideMenuItem from './SideMenuItem';
8
9
 
10
+ // Context for passing LinkComponent to SideMenuItem children
11
+ const SideMenuContext = createContext<{
12
+ LinkComponent?: React.ComponentType<{
13
+ href?: string;
14
+ to?: string;
15
+ children: React.ReactNode;
16
+ className?: string;
17
+ onClick?: (event: React.MouseEvent) => void;
18
+ target?: string;
19
+ rel?: string;
20
+ 'aria-disabled'?: boolean;
21
+ 'aria-current'?: string;
22
+ tabIndex?: number;
23
+ ref?: React.Ref<HTMLAnchorElement>;
24
+ }>;
25
+ }>({});
26
+
27
+ // Hook to use SideMenu context
28
+ export const useSideMenuContext = () => useContext(SideMenuContext);
29
+
9
30
  /**
10
31
  * SideMenu component provides a collapsible navigation menu with title and menu items.
11
32
  * Automatically collapses on mobile devices and can be toggled via a header button.
@@ -38,6 +59,7 @@ export const SideMenu = forwardRef<HTMLDivElement, SideMenuProps>(
38
59
  toggleIcon,
39
60
  id,
40
61
  glass,
62
+ LinkComponent,
41
63
  },
42
64
  ref
43
65
  ) => {
@@ -49,7 +71,6 @@ export const SideMenu = forwardRef<HTMLDivElement, SideMenuProps>(
49
71
  generateSideMenuClass,
50
72
  generateWrapperClass,
51
73
  handleToggle,
52
- handleDesktopCollapse,
53
74
  } = useSideMenu({
54
75
  isOpen,
55
76
  onToggle,
@@ -59,6 +80,7 @@ export const SideMenu = forwardRef<HTMLDivElement, SideMenuProps>(
59
80
  disabled,
60
81
  });
61
82
 
83
+ // Mobile breakpoint matches md breakpoint (768px)
62
84
  const MOBILE_BREAKPOINT = 768;
63
85
 
64
86
  // Track mobile state
@@ -116,23 +138,23 @@ export const SideMenu = forwardRef<HTMLDivElement, SideMenuProps>(
116
138
  });
117
139
  }, [menuItems?.length]);
118
140
 
141
+ // Helper function to update nested wrapper height
142
+ const updateNestedHeight = (index: number, isOpen: boolean) => {
143
+ const wrapper = nestedWrapperRefs.current[index];
144
+ const inner = nestedInnerRefs.current[index];
145
+ if (wrapper && inner) {
146
+ wrapper.style.height = isOpen ? `${inner.scrollHeight}px` : '0px';
147
+ }
148
+ };
149
+
119
150
  // Set initial heights for nested wrappers on mount and when menuItems change
120
151
  useEffect(() => {
121
152
  if (!menuItems?.length) return;
122
153
 
123
154
  const timeoutId = setTimeout(() => {
124
155
  menuItems.forEach((_, index) => {
125
- const wrapper = nestedWrapperRefs.current[index];
126
- const inner = nestedInnerRefs.current[index];
127
156
  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
- }
157
+ updateNestedHeight(index, isOpen);
136
158
  });
137
159
  }, 0);
138
160
 
@@ -149,22 +171,12 @@ export const SideMenu = forwardRef<HTMLDivElement, SideMenuProps>(
149
171
 
150
172
  Object.keys(nestedItemStates).forEach(key => {
151
173
  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
- }
174
+ const isOpen = nestedItemStates[index] ?? true;
175
+
176
+ const frameId = requestAnimationFrame(() => {
177
+ updateNestedHeight(index, isOpen);
178
+ });
179
+ frameIds.push(frameId);
168
180
  });
169
181
 
170
182
  return () => {
@@ -172,15 +184,8 @@ export const SideMenu = forwardRef<HTMLDivElement, SideMenuProps>(
172
184
  };
173
185
  }, [nestedItemStates, menuItems?.length]);
174
186
 
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
- };
187
+ // Combine refs using utility
188
+ const combinedRef = useForkRef(sideMenuRef, ref);
184
189
 
185
190
  const sideMenuClass = generateSideMenuClass({
186
191
  className,
@@ -230,8 +235,9 @@ export const SideMenu = forwardRef<HTMLDivElement, SideMenuProps>(
230
235
  id={id ? `${id}-content` : undefined}
231
236
  aria-hidden={shouldShowToggler ? !isOpenState : false}
232
237
  >
233
- <div ref={innerRef} className="c-side-menu__inner">
234
- {children && children}
238
+ <SideMenuContext.Provider value={{ LinkComponent }}>
239
+ <div ref={innerRef} className="c-side-menu__inner">
240
+ {children}
235
241
  {menuItems?.map((item, index) => {
236
242
  const isNestedItemOpen = nestedItemStates[index] ?? true;
237
243
  const hasItems = item.items && item.items.length > 0;
@@ -302,6 +308,7 @@ export const SideMenu = forwardRef<HTMLDivElement, SideMenuProps>(
302
308
  active={subItem.active}
303
309
  disabled={subItem.disabled}
304
310
  icon={subItem.icon}
311
+ LinkComponent={LinkComponent}
305
312
  >
306
313
  {subItem.title}
307
314
  </SideMenuItem>
@@ -313,7 +320,8 @@ export const SideMenu = forwardRef<HTMLDivElement, SideMenuProps>(
313
320
  </div>
314
321
  );
315
322
  })}
316
- </div>
323
+ </div>
324
+ </SideMenuContext.Provider>
317
325
  </div>
318
326
  </>
319
327
  );
@@ -330,7 +338,7 @@ export const SideMenu = forwardRef<HTMLDivElement, SideMenuProps>(
330
338
  <AtomixGlass {...glassProps}>
331
339
  <div
332
340
  ref={combinedRef}
333
- className={sideMenuClass + ' c-side-menu--glass'}
341
+ className={`${sideMenuClass} c-side-menu--glass`}
334
342
  id={id}
335
343
  style={style}
336
344
  >
@@ -1,6 +1,7 @@
1
1
  import React, { forwardRef } from 'react';
2
2
  import { SideMenuItemProps } from '../../../lib/types/components';
3
3
  import { useSideMenuItem } from '../../../lib/composables/useSideMenu';
4
+ import { useSideMenuContext } from './SideMenu';
4
5
 
5
6
  /**
6
7
  * SideMenuItem component represents a single navigation item in a side menu.
@@ -39,10 +40,14 @@ export const SideMenuItem = forwardRef<HTMLAnchorElement | HTMLButtonElement, Si
39
40
  className = '',
40
41
  target,
41
42
  rel,
42
- LinkComponent,
43
+ LinkComponent: LinkComponentProp,
43
44
  },
44
45
  ref
45
46
  ) => {
47
+ const { LinkComponent: LinkComponentFromContext } = useSideMenuContext();
48
+ // Use LinkComponent from props first, then fall back to context
49
+ const LinkComponent = LinkComponentProp ?? LinkComponentFromContext;
50
+
46
51
  const { generateSideMenuItemClass, handleClick } = useSideMenuItem({
47
52
  active,
48
53
  disabled,
@@ -51,31 +56,65 @@ export const SideMenuItem = forwardRef<HTMLAnchorElement | HTMLButtonElement, Si
51
56
 
52
57
  const itemClass = generateSideMenuItemClass();
53
58
 
54
- const linkProps = {
55
- ref: ref as React.Ref<HTMLAnchorElement>,
56
- href: disabled ? undefined : href,
57
- className: itemClass,
58
- onClick: handleClick(onClick),
59
- 'aria-disabled': disabled,
60
- 'aria-current': (active ? 'page' : undefined) as React.AriaAttributes['aria-current'],
61
- target: target,
62
- rel: rel,
63
- tabIndex: disabled ? -1 : 0,
64
- };
65
-
66
59
  // Render as link if href is provided
67
60
  if (href) {
68
- return LinkComponent ? (
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
- })()
78
- ) : (
61
+ // When using a custom LinkComponent (e.g., Next.js Link, React Router Link)
62
+ if (LinkComponent) {
63
+ const Component = LinkComponent;
64
+
65
+ // Build link props - support both 'href' (Next.js) and 'to' (React Router)
66
+ // The Link component will use whichever prop it needs
67
+ const linkProps: {
68
+ ref?: React.Ref<HTMLAnchorElement>;
69
+ className?: string;
70
+ onClick?: (event: React.MouseEvent) => void;
71
+ 'aria-disabled'?: boolean;
72
+ 'aria-current'?: string;
73
+ target?: string;
74
+ rel?: string;
75
+ tabIndex?: number;
76
+ href?: string;
77
+ to?: string;
78
+ } = {
79
+ ref: ref as React.Ref<HTMLAnchorElement>,
80
+ className: itemClass,
81
+ onClick: disabled
82
+ ? (e: React.MouseEvent) => {
83
+ e.preventDefault();
84
+ }
85
+ : onClick,
86
+ 'aria-disabled': disabled,
87
+ 'aria-current': active ? 'page' : undefined,
88
+ target: target,
89
+ rel: rel,
90
+ tabIndex: disabled ? -1 : 0,
91
+ // Support both Next.js (href) and React Router (to) Link components
92
+ // Pass both props - the Link component will use whichever it needs
93
+ ...(disabled ? {} : { href, to: href }),
94
+ };
95
+
96
+ return (
97
+ <Component {...linkProps}>
98
+ {icon && <span className="c-side-menu__link-icon">{icon}</span>}
99
+ <span className="c-side-menu__link-text">{children}</span>
100
+ </Component>
101
+ );
102
+ }
103
+
104
+ // Regular anchor tag
105
+ const linkProps = {
106
+ ref: ref as React.Ref<HTMLAnchorElement>,
107
+ href: disabled ? undefined : href,
108
+ className: itemClass,
109
+ onClick: handleClick(onClick),
110
+ 'aria-disabled': disabled,
111
+ 'aria-current': (active ? 'page' : undefined) as React.AriaAttributes['aria-current'],
112
+ target: target,
113
+ rel: rel,
114
+ tabIndex: disabled ? -1 : 0,
115
+ };
116
+
117
+ return (
79
118
  <a {...linkProps}>
80
119
  {icon && <span className="c-side-menu__link-icon">{icon}</span>}
81
120
  <span className="c-side-menu__link-text">{children}</span>
@@ -94,7 +94,7 @@ export const Popover: React.FC<PopoverProps> = ({
94
94
  glass === true ? defaultGlassProps : { ...defaultGlassProps, ...glass };
95
95
 
96
96
  return (
97
- <AtomixGlass {...glassProps}>
97
+ <AtomixGlass {...glassProps} style={style}>
98
98
  <div className="c-popover__content">
99
99
  <div className="c-popover__content-inner">{content}</div>
100
100
  </div>