@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.
- package/dist/atomix.css +309 -105
- package/dist/atomix.min.css +3 -5
- package/dist/index.d.ts +804 -53
- package/dist/index.esm.js +16367 -16413
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +16275 -16336
- 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 +309 -105
- package/dist/themes/applemix.min.css +5 -7
- package/dist/themes/boomdevs.css +202 -10
- package/dist/themes/boomdevs.min.css +3 -5
- package/dist/themes/esrar.css +309 -105
- package/dist/themes/esrar.min.css +4 -6
- package/dist/themes/flashtrade.css +310 -105
- package/dist/themes/flashtrade.min.css +5 -7
- package/dist/themes/mashroom.css +300 -96
- package/dist/themes/mashroom.min.css +4 -6
- package/dist/themes/shaj-default.css +300 -96
- package/dist/themes/shaj-default.min.css +4 -6
- package/package.json +1 -1
- package/src/components/AtomixGlass/AtomixGlass.test.tsx +21 -32
- package/src/components/AtomixGlass/AtomixGlass.tsx +55 -42
- package/src/components/AtomixGlass/AtomixGlassContainer.tsx +205 -57
- package/src/components/AtomixGlass/GlassFilter.tsx +22 -8
- package/src/components/AtomixGlass/__snapshots__/AtomixGlass.test.tsx.snap +221 -0
- package/src/components/AtomixGlass/atomixGLass.old.tsx +0 -3
- package/src/components/AtomixGlass/shader-utils.ts +8 -0
- package/src/components/AtomixGlass/stories/AtomixGlass.stories.tsx +319 -100
- package/src/components/AtomixGlass/stories/Examples.stories.tsx +601 -105
- package/src/components/AtomixGlass/stories/Modes.stories.tsx +30 -12
- package/src/components/AtomixGlass/stories/Playground.stories.tsx +173 -38
- package/src/components/AtomixGlass/stories/ShaderVariants.stories.tsx +18 -18
- package/src/components/AtomixGlass/stories/shared-components.tsx +27 -5
- package/src/components/Button/Button.tsx +62 -17
- package/src/components/Callout/Callout.test.tsx +8 -14
- package/src/components/Card/Card.tsx +103 -1
- package/src/components/Card/index.ts +3 -2
- package/src/components/Icon/index.ts +1 -1
- package/src/components/Modal/Modal.stories.tsx +29 -38
- package/src/components/Modal/Modal.tsx +4 -4
- package/src/components/Navigation/SideMenu/SideMenu.tsx +49 -41
- package/src/components/Navigation/SideMenu/SideMenuItem.tsx +63 -24
- package/src/components/Popover/Popover.tsx +1 -1
- package/src/components/VideoPlayer/VideoPlayer.stories.tsx +977 -400
- package/src/components/VideoPlayer/VideoPlayer.tsx +1 -6
- package/src/lib/composables/shared-mouse-tracker.ts +133 -0
- package/src/lib/composables/useAtomixGlass.ts +303 -115
- package/src/lib/theme/ThemeManager.integration.test.ts +124 -0
- package/src/lib/theme/ThemeManager.stories.tsx +13 -13
- package/src/lib/theme/ThemeManager.test.ts +4 -0
- package/src/lib/theme/ThemeManager.ts +203 -59
- package/src/lib/theme/ThemeProvider.tsx +183 -33
- package/src/lib/theme/composeTheme.ts +375 -0
- package/src/lib/theme/createTheme.test.ts +475 -0
- package/src/lib/theme/createTheme.ts +510 -0
- package/src/lib/theme/generateCSSVariables.ts +713 -0
- package/src/lib/theme/index.ts +67 -0
- package/src/lib/theme/themeUtils.ts +333 -0
- package/src/lib/theme/types.ts +337 -8
- package/src/lib/theme/useTheme.test.tsx +2 -1
- package/src/lib/theme/useTheme.ts +6 -22
- package/src/lib/types/components.ts +148 -59
- package/src/styles/01-settings/_index.scss +2 -2
- package/src/styles/01-settings/_settings.badge.scss +2 -2
- package/src/styles/01-settings/_settings.border-radius.scss +1 -1
- package/src/styles/01-settings/{_settings.maps.scss → _settings.design-tokens.scss} +163 -49
- package/src/styles/01-settings/_settings.modal.scss +1 -1
- package/src/styles/01-settings/_settings.spacing.scss +14 -13
- package/src/styles/03-generic/_generic.root.scss +131 -50
- package/src/styles/05-objects/_objects.block.scss +1 -1
- package/src/styles/06-components/_components.atomix-glass.scss +20 -22
- package/src/styles/06-components/_components.badge.scss +2 -2
- package/src/styles/06-components/_components.button.scss +1 -1
- package/src/styles/06-components/_components.callout.scss +1 -1
- package/src/styles/06-components/_components.card.scss +74 -2
- package/src/styles/06-components/_components.chart.scss +1 -1
- package/src/styles/06-components/_components.dropdown.scss +6 -0
- package/src/styles/06-components/_components.footer.scss +1 -1
- package/src/styles/06-components/_components.list-group.scss +1 -1
- package/src/styles/06-components/_components.list.scss +1 -1
- package/src/styles/06-components/_components.menu.scss +1 -1
- package/src/styles/06-components/_components.messages.scss +1 -1
- package/src/styles/06-components/_components.modal.scss +7 -2
- package/src/styles/06-components/_components.navbar.scss +1 -1
- package/src/styles/06-components/_components.popover.scss +10 -0
- package/src/styles/06-components/_components.product-review.scss +1 -1
- package/src/styles/06-components/_components.progress.scss +1 -1
- package/src/styles/06-components/_components.rating.scss +1 -1
- package/src/styles/06-components/_components.spinner.scss +1 -1
- package/src/styles/99-utilities/_utilities.background.scss +1 -1
- package/src/styles/99-utilities/_utilities.border.scss +1 -1
- package/src/styles/99-utilities/_utilities.link.scss +1 -1
- 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:
|
|
155
|
-
blurAmount: 0,
|
|
156
|
-
saturation: 160,
|
|
157
|
-
aberrationIntensity: 1,
|
|
154
|
+
displacementScale: 30,
|
|
158
155
|
cornerRadius: 8,
|
|
159
|
-
|
|
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
|
|
188
|
-
|
|
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
|
|
228
|
-
expect(
|
|
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: '
|
|
333
|
-
|
|
334
|
-
|
|
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: '
|
|
527
|
-
|
|
528
|
-
|
|
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:
|
|
201
|
-
blurAmount: 2,
|
|
202
|
-
|
|
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
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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 = (
|
|
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
|
-
<
|
|
234
|
-
{
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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>
|