@shohojdhara/atomix 0.5.1 → 0.5.2

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 (123) hide show
  1. package/atomix.config.ts +12 -0
  2. package/build-tools/webpack-loader.js +5 -4
  3. package/dist/atomix.css +138 -17
  4. package/dist/atomix.css.map +1 -1
  5. package/dist/atomix.min.css +1 -1
  6. package/dist/atomix.min.css.map +1 -1
  7. package/dist/build-tools/webpack-loader.js +5 -4
  8. package/dist/charts.d.ts +23 -23
  9. package/dist/charts.js +40 -37
  10. package/dist/charts.js.map +1 -1
  11. package/dist/config.d.ts +624 -0
  12. package/dist/config.js +59 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/core.d.ts +2 -2
  15. package/dist/core.js +111 -50
  16. package/dist/core.js.map +1 -1
  17. package/dist/forms.d.ts +3 -6
  18. package/dist/forms.js +2 -2
  19. package/dist/forms.js.map +1 -1
  20. package/dist/heavy.d.ts +1 -1
  21. package/dist/heavy.js +173 -111
  22. package/dist/heavy.js.map +1 -1
  23. package/dist/index.d.ts +98 -65
  24. package/dist/index.esm.js +427 -422
  25. package/dist/index.esm.js.map +1 -1
  26. package/dist/index.js +394 -391
  27. package/dist/index.js.map +1 -1
  28. package/dist/index.min.js +1 -1
  29. package/dist/index.min.js.map +1 -1
  30. package/dist/layout.js +59 -60
  31. package/dist/layout.js.map +1 -1
  32. package/dist/theme.js +4 -4
  33. package/dist/theme.js.map +1 -1
  34. package/package.json +14 -9
  35. package/scripts/atomix-cli.js +15 -1
  36. package/scripts/cli/__tests__/complexity-utils.test.js +24 -0
  37. package/scripts/cli/__tests__/detector.test.js +50 -0
  38. package/scripts/cli/__tests__/template-engine.test.js +23 -0
  39. package/scripts/cli/__tests__/test-setup.js +3 -0
  40. package/scripts/cli/commands/doctor.js +15 -3
  41. package/scripts/cli/commands/generate.js +113 -51
  42. package/scripts/cli/internal/ai-engine.js +30 -10
  43. package/scripts/cli/internal/complexity-utils.js +60 -0
  44. package/scripts/cli/internal/component-validator.js +49 -16
  45. package/scripts/cli/internal/generator.js +89 -36
  46. package/scripts/cli/internal/hook-generator.js +5 -2
  47. package/scripts/cli/internal/itcss-generator.js +16 -12
  48. package/scripts/cli/templates/next-templates.js +81 -30
  49. package/scripts/cli/templates/storybook-templates.js +12 -2
  50. package/scripts/cli/utils/detector.js +45 -7
  51. package/scripts/cli/utils/diagnostics.js +78 -0
  52. package/scripts/cli/utils/telemetry.js +13 -0
  53. package/src/components/Accordion/Accordion.stories.tsx +4 -0
  54. package/src/components/AtomixGlass/AtomixGlassContainer.tsx +1 -1
  55. package/src/components/AtomixGlass/__snapshots__/AtomixGlass.test.tsx.snap +219 -0
  56. package/src/components/AtomixGlass/glass-utils.ts +1 -1
  57. package/src/components/Button/Button.tsx +114 -57
  58. package/src/components/Callout/Callout.tsx +4 -4
  59. package/src/components/Chart/ChartRenderer.tsx +1 -1
  60. package/src/components/Chart/DonutChart.tsx +11 -8
  61. package/src/components/EdgePanel/EdgePanel.tsx +119 -115
  62. package/src/components/Form/Select.tsx +4 -4
  63. package/src/components/List/List.tsx +4 -4
  64. package/src/components/Navigation/SideMenu/SideMenu.tsx +6 -6
  65. package/src/components/PhotoViewer/PhotoViewerImage.tsx +1 -1
  66. package/src/components/ProductReview/ProductReview.tsx +4 -2
  67. package/src/components/Rating/Rating.tsx +4 -2
  68. package/src/components/SectionIntro/SectionIntro.tsx +4 -2
  69. package/src/components/Steps/Steps.tsx +1 -1
  70. package/src/components/Tabs/Tabs.tsx +5 -5
  71. package/src/components/Testimonial/Testimonial.tsx +4 -2
  72. package/src/components/VideoPlayer/VideoPlayer.tsx +4 -2
  73. package/src/layouts/CssGrid/CssGrid.stories.tsx +464 -0
  74. package/src/layouts/CssGrid/CssGrid.tsx +215 -0
  75. package/src/layouts/CssGrid/index.ts +8 -0
  76. package/src/layouts/CssGrid/scripts/CssGrid.js +284 -0
  77. package/src/layouts/CssGrid/scripts/index.js +43 -0
  78. package/src/layouts/Grid/scripts/Container.js +139 -0
  79. package/src/layouts/Grid/scripts/Grid.js +184 -0
  80. package/src/layouts/Grid/scripts/GridCol.js +273 -0
  81. package/src/layouts/Grid/scripts/Row.js +154 -0
  82. package/src/layouts/Grid/scripts/index.js +48 -0
  83. package/src/layouts/MasonryGrid/MasonryGrid.tsx +71 -59
  84. package/src/lib/composables/atomix-glass/useGlassSize.ts +1 -1
  85. package/src/lib/composables/useAccordion.ts +5 -5
  86. package/src/lib/composables/useAtomixGlass.ts +3 -3
  87. package/src/lib/composables/useBarChart.ts +2 -2
  88. package/src/lib/composables/useChart.ts +3 -2
  89. package/src/lib/composables/useChartToolbar.ts +48 -66
  90. package/src/lib/composables/useDataTable.ts +1 -1
  91. package/src/lib/composables/useDatePicker.ts +2 -2
  92. package/src/lib/composables/useEdgePanel.ts +45 -54
  93. package/src/lib/composables/useHeroBackgroundSlider.ts +5 -5
  94. package/src/lib/composables/usePhotoViewer.ts +2 -3
  95. package/src/lib/composables/usePieChart.ts +1 -1
  96. package/src/lib/composables/usePopover.ts +151 -139
  97. package/src/lib/composables/useSideMenu.ts +28 -41
  98. package/src/lib/composables/useSlider.ts +2 -6
  99. package/src/lib/composables/useTooltip.ts +2 -2
  100. package/src/lib/config/index.ts +39 -0
  101. package/src/lib/theme/devtools/Comparator.tsx +1 -1
  102. package/src/lib/theme/devtools/Inspector.tsx +1 -1
  103. package/src/lib/theme/devtools/LiveEditor.tsx +1 -1
  104. package/src/lib/theme/runtime/ThemeProvider.tsx +1 -1
  105. package/src/styles/01-settings/_index.scss +1 -0
  106. package/src/styles/01-settings/_settings.atomix-glass.scss +174 -0
  107. package/src/styles/01-settings/_settings.masonry-grid.scss +42 -6
  108. package/src/styles/02-tools/_tools.glass.scss +6 -0
  109. package/src/styles/05-objects/_objects.masonry-grid.scss +162 -24
  110. package/src/styles/06-components/_components.atomix-glass.scss +4 -4
  111. package/src/lib/composables/useBreadcrumb.ts +0 -81
  112. package/src/lib/composables/useChartInteractions.ts +0 -123
  113. package/src/lib/composables/useChartPerformance.ts +0 -347
  114. package/src/lib/composables/useDropdown.ts +0 -338
  115. package/src/lib/composables/useModal.ts +0 -110
  116. package/src/lib/hooks/usePerformanceMonitor.ts +0 -148
  117. package/src/lib/utils/displacement-generator.ts +0 -92
  118. package/src/lib/utils/memoryMonitor.ts +0 -191
  119. package/src/styles/01-settings/_settings.testtypecheck.scss +0 -53
  120. package/src/styles/01-settings/_settings.typedbutton.scss +0 -53
  121. package/src/styles/06-components/_components.testbutton.scss +0 -212
  122. package/src/styles/06-components/_components.testtypecheck.scss +0 -212
  123. package/src/styles/06-components/_components.typedbutton.scss +0 -212
@@ -5,6 +5,7 @@ import { Spinner } from '../Spinner/Spinner';
5
5
  import { Icon, type PhosphorIconsType } from '../Icon/Icon';
6
6
  import { BUTTON, THEME_NAMING } from '../../lib/constants/components';
7
7
  import { ThemeNaming } from '../../lib/utils/themeNaming';
8
+ import { renderSlot } from '../../lib/patterns';
8
9
 
9
10
  export type ButtonAsProp = {
10
11
  as?: ElementType;
@@ -52,6 +53,7 @@ export const Button = React.memo(
52
53
  tabIndex,
53
54
  style,
54
55
  linkComponent,
56
+ slots,
55
57
  ...props
56
58
  },
57
59
  ref
@@ -161,17 +163,34 @@ export const Button = React.memo(
161
163
  )}
162
164
  aria-hidden="true"
163
165
  >
164
- <Spinner
165
- size={spinnerSize}
166
- variant={
167
- variant === 'link' ||
168
- (typeof variant === 'string' && variant.startsWith('outline-'))
169
- ? 'primary'
170
- : variant === 'danger'
171
- ? 'error'
172
- : (variant as any)
173
- }
174
- />
166
+ {renderSlot(
167
+ slots?.spinner,
168
+ {
169
+ className: ThemeNaming.bemClass(
170
+ THEME_NAMING.BUTTON_PREFIX,
171
+ THEME_NAMING.SPINNER_ELEMENT
172
+ ),
173
+ size: spinnerSize,
174
+ variant:
175
+ variant === 'link' ||
176
+ (typeof variant === 'string' && variant.startsWith('outline-'))
177
+ ? 'primary'
178
+ : variant === 'danger'
179
+ ? 'error'
180
+ : (variant as any),
181
+ } as any,
182
+ <Spinner
183
+ size={spinnerSize}
184
+ variant={
185
+ variant === 'link' ||
186
+ (typeof variant === 'string' && variant.startsWith('outline-'))
187
+ ? 'primary'
188
+ : variant === 'danger'
189
+ ? 'error'
190
+ : (variant as any)
191
+ }
192
+ />
193
+ )}
175
194
  </span>
176
195
  )}
177
196
  {iconElement && !loading && (
@@ -182,7 +201,18 @@ export const Button = React.memo(
182
201
  )}
183
202
  aria-hidden="true"
184
203
  >
185
- {iconElement}
204
+ {renderSlot(
205
+ slots?.icon,
206
+ {
207
+ className: ThemeNaming.bemClass(
208
+ THEME_NAMING.BUTTON_PREFIX,
209
+ THEME_NAMING.ICON_ELEMENT
210
+ ),
211
+ children: iconElement,
212
+ size: iconSize,
213
+ } as any,
214
+ iconElement
215
+ )}
186
216
  </span>
187
217
  )}
188
218
  {!iconOnly && buttonText && (
@@ -192,7 +222,17 @@ export const Button = React.memo(
192
222
  THEME_NAMING.LABEL_ELEMENT
193
223
  )}
194
224
  >
195
- {buttonText}
225
+ {renderSlot(
226
+ slots?.label,
227
+ {
228
+ className: ThemeNaming.bemClass(
229
+ THEME_NAMING.BUTTON_PREFIX,
230
+ THEME_NAMING.LABEL_ELEMENT
231
+ ),
232
+ children: buttonText,
233
+ } as any,
234
+ buttonText
235
+ )}
196
236
  </span>
197
237
  )}
198
238
  </>
@@ -218,48 +258,65 @@ export const Button = React.memo(
218
258
 
219
259
  let content: React.ReactElement;
220
260
 
221
- // Render as anchor if href is provided
222
- if (shouldRenderAsLink) {
223
- // Use custom linkComponent if provided (e.g., Next.js Link)
224
- if (linkComponent) {
225
- const LinkComp = linkComponent as React.ComponentType<any>;
226
- const linkProps = {
227
- ...buttonProps,
228
- ref: ref as any, // linkComponent usually forwards ref to anchor
229
- href: isDisabled ? undefined : href,
230
- to: isDisabled ? undefined : href,
231
- target,
232
- rel: target === '_blank' ? 'noopener noreferrer' : undefined,
233
- };
261
+ // Render the button content using the root slot if provided
262
+ const buttonChildren = renderSlot(
263
+ slots?.root,
264
+ {
265
+ className: buttonClass,
266
+ children: buttonContent,
267
+ disabled: isDisabled,
268
+ loading: loading,
269
+ onClick: handleClickEvent,
270
+ type: type,
271
+ 'aria-label': safeAriaLabel,
272
+ 'aria-disabled': isDisabled,
273
+ 'aria-busy': loading,
274
+ } as any,
275
+ (() => {
276
+ // Render as anchor if href is provided
277
+ if (shouldRenderAsLink) {
278
+ // Use custom linkComponent if provided (e.g., Next.js Link)
279
+ if (linkComponent) {
280
+ const LinkComp = linkComponent as React.ComponentType<any>;
281
+ const linkProps = {
282
+ ...buttonProps,
283
+ ref: ref as any, // linkComponent usually forwards ref to anchor
284
+ href: isDisabled ? undefined : href,
285
+ to: isDisabled ? undefined : href,
286
+ target,
287
+ rel: target === '_blank' ? 'noopener noreferrer' : undefined,
288
+ };
234
289
 
235
- content = <LinkComp {...linkProps}>{buttonContent}</LinkComp>;
236
- } else {
237
- // Fallback to regular anchor tag
238
- content = (
239
- <a
240
- {...buttonProps}
241
- ref={ref as React.Ref<HTMLAnchorElement>}
242
- href={isDisabled ? undefined : href}
243
- target={target}
244
- rel={target === '_blank' ? 'noopener noreferrer' : undefined}
245
- >
246
- {buttonContent}
247
- </a>
248
- );
249
- }
250
- } else {
251
- // Default button rendering
252
- content = (
253
- <Component
254
- {...buttonProps}
255
- ref={ref}
256
- type={Component === 'button' ? type : undefined}
257
- disabled={isDisabled}
258
- >
259
- {buttonContent}
260
- </Component>
261
- );
262
- }
290
+ return <LinkComp {...linkProps}>{buttonContent}</LinkComp>;
291
+ } else {
292
+ // Fallback to regular anchor tag
293
+ return (
294
+ <a
295
+ {...buttonProps}
296
+ ref={ref as React.Ref<HTMLAnchorElement>}
297
+ href={isDisabled ? undefined : href}
298
+ target={target}
299
+ rel={target === '_blank' ? 'noopener noreferrer' : undefined}
300
+ >
301
+ {buttonContent}
302
+ </a>
303
+ );
304
+ }
305
+ } else {
306
+ // Default button rendering
307
+ return (
308
+ <Component
309
+ {...buttonProps}
310
+ ref={ref}
311
+ type={Component === 'button' ? type : undefined}
312
+ disabled={isDisabled}
313
+ >
314
+ {buttonContent}
315
+ </Component>
316
+ );
317
+ }
318
+ })()
319
+ );
263
320
 
264
321
  if (glass) {
265
322
  // Default glass props
@@ -270,10 +327,10 @@ export const Button = React.memo(
270
327
  elasticity: 0,
271
328
  };
272
329
  const glassProps = glass === true ? defaultGlassProps : { ...defaultGlassProps, ...glass };
273
- return <AtomixGlass {...glassProps}>{content}</AtomixGlass>;
330
+ return <AtomixGlass {...glassProps}>{buttonChildren}</AtomixGlass>;
274
331
  }
275
332
 
276
- return content;
333
+ return buttonChildren;
277
334
  }
278
335
  )
279
336
  );
@@ -282,4 +339,4 @@ Button.displayName = 'Button';
282
339
 
283
340
  export type { ButtonProps };
284
341
 
285
- export default Button;
342
+ export default Button;
@@ -89,8 +89,7 @@ type CalloutComponent = React.FC<CalloutProps> & {
89
89
  Content: typeof CalloutContent;
90
90
  };
91
91
 
92
- export const Callout: CalloutComponent = memo(
93
- ({
92
+ const CalloutComponentBase = ({
94
93
  title,
95
94
  children,
96
95
  icon,
@@ -212,8 +211,9 @@ export const Callout: CalloutComponent = memo(
212
211
  {calloutContent}
213
212
  </div>
214
213
  );
215
- }
216
- ) as unknown as CalloutComponent;
214
+ };
215
+
216
+ export const Callout = memo(CalloutComponentBase) as unknown as CalloutComponent;
217
217
 
218
218
  Callout.displayName = 'Callout';
219
219
 
@@ -273,7 +273,7 @@ const ChartRenderer = memo(
273
273
  const transform = useMemo(() => {
274
274
  if (!chartContext) return '';
275
275
  return `translate(${chartContext.panOffset.x}px, ${chartContext.panOffset.y}px) scale(${chartContext.zoomLevel})`;
276
- }, [chartContext?.panOffset.x, chartContext?.panOffset.y, chartContext?.zoomLevel]);
276
+ }, [chartContext]);
277
277
 
278
278
  // Calculate chart data with enhanced features using responsive dimensions
279
279
  // This MUST be called before any early returns to maintain consistent hook order
@@ -2,7 +2,7 @@ import { forwardRef, memo, useMemo } from 'react';
2
2
  import BaseChart from './BaseChart';
3
3
  import ChartTooltip from './ChartTooltip';
4
4
  import { PieChartProps } from './PieChart';
5
- import { ChartDataPoint, ChartRenderContentParams } from './types';
5
+ import { ChartDataPoint, ChartRenderContentParams, ChartDataset } from './types';
6
6
 
7
7
  interface DonutChartProps extends Omit<PieChartProps, 'type'> {
8
8
  /**
@@ -63,15 +63,18 @@ const DonutChart = memo(
63
63
  ref
64
64
  ) => {
65
65
  // Use the first dataset for donut chart
66
- const dataset = datasets.length > 0 ? datasets[0] : { label: '', data: [] };
66
+ const dataset = useMemo(
67
+ () => (datasets.length > 0 ? (datasets[0] as ChartDataset) : { label: '', data: [] as ChartDataPoint[] }),
68
+ [datasets]
69
+ );
67
70
 
68
71
  // Prepare data for donut chart (calculations will be done in renderContent with actual dimensions)
69
72
  const chartData = useMemo(() => {
70
73
  if (!dataset?.data?.length) return null;
71
74
 
72
75
  // Filter out invalid data points
73
- const validDataPoints = dataset?.data?.filter(
74
- point =>
76
+ const validDataPoints = (dataset?.data || []).filter(
77
+ (point: ChartDataPoint) =>
75
78
  typeof point.value === 'number' &&
76
79
  !isNaN(point.value) &&
77
80
  isFinite(point.value) &&
@@ -117,16 +120,16 @@ const DonutChart = memo(
117
120
 
118
121
  const chartColors = dataset?.color
119
122
  ? [dataset.color]
120
- : dataset?.data?.map((_, i) => defaultColors[i % defaultColors.length]) || defaultColors;
123
+ : dataset?.data?.map((_, i: number) => defaultColors[i % defaultColors.length]) || defaultColors;
121
124
 
122
125
  // Calculate total value
123
- const total = chartData.validDataPoints.reduce((sum, point) => sum + point.value, 0);
126
+ const total = chartData.validDataPoints.reduce((sum: number, point: ChartDataPoint) => sum + point.value, 0);
124
127
 
125
128
  // Calculate angles for each slice
126
129
  const padAngleRad = ((pieOptions.padAngle || 1) * Math.PI) / 180;
127
130
  let currentAngle = ((pieOptions.startAngle || 0) * Math.PI) / 180;
128
131
 
129
- const slices = chartData.validDataPoints.map((point, index) => {
132
+ const slices = chartData.validDataPoints.map((point: ChartDataPoint, index: number) => {
130
133
  const percentage = point.value / total;
131
134
  const sliceAngle = percentage * (2 * Math.PI) - padAngleRad;
132
135
  const startAngle = currentAngle;
@@ -176,7 +179,7 @@ const DonutChart = memo(
176
179
 
177
180
  return (
178
181
  <>
179
- {slices.map((slice, index) => {
182
+ {slices.map((slice: any, index: number) => {
180
183
  const isHovered = hoveredPoint?.pointIndex === index;
181
184
 
182
185
  return (
@@ -33,19 +33,20 @@ export const EdgePanelFooter = forwardRef<HTMLDivElement, React.HTMLAttributes<H
33
33
  );
34
34
  EdgePanelFooter.displayName = 'EdgePanelFooter';
35
35
 
36
- export const EdgePanelCloseButton = forwardRef<HTMLButtonElement, React.ButtonHTMLAttributes<HTMLButtonElement>>(
37
- ({ className = '', onClick, ...props }, ref) => (
38
- <button
39
- ref={ref}
40
- className={`c-edge-panel__close c-btn c-btn--icon ${className}`.trim()}
41
- onClick={onClick}
42
- aria-label="Close panel"
43
- {...props}
44
- >
45
- <Icon name="X" />
46
- </button>
47
- )
48
- );
36
+ export const EdgePanelCloseButton = forwardRef<
37
+ HTMLButtonElement,
38
+ React.ButtonHTMLAttributes<HTMLButtonElement>
39
+ >(({ className = '', onClick, ...props }, ref) => (
40
+ <button
41
+ ref={ref}
42
+ className={`c-edge-panel__close c-btn c-btn--icon ${className}`.trim()}
43
+ onClick={onClick}
44
+ aria-label="Close panel"
45
+ {...props}
46
+ >
47
+ <Icon name="X" />
48
+ </button>
49
+ ));
49
50
  EdgePanelCloseButton.displayName = 'EdgePanelCloseButton';
50
51
 
51
52
  /**
@@ -82,113 +83,116 @@ type EdgePanelComponent = React.FC<EdgePanelProps> & {
82
83
  CloseButton: typeof EdgePanelCloseButton;
83
84
  };
84
85
 
85
- export const EdgePanel: EdgePanelComponent = memo(
86
- ({
87
- title,
88
- children,
89
- position = 'start',
90
- mode = 'slide',
91
- isOpen = false,
86
+ const EdgePanelComponentBase = ({
87
+ title,
88
+ children,
89
+ position = 'start',
90
+ mode = 'slide',
91
+ isOpen = false,
92
+ onOpenChange,
93
+ backdrop = true,
94
+ closeOnBackdropClick = true,
95
+ closeOnEscape = true,
96
+ className = '',
97
+ style,
98
+ glass,
99
+ }: EdgePanelProps) => {
100
+ const {
101
+ isOpen: isOpenState,
102
+ containerRef,
103
+ backdropRef,
104
+ generateEdgePanelClass,
105
+ closePanel,
106
+ handleBackdropClick,
107
+ } = useEdgePanel({
108
+ position,
109
+ mode,
110
+ isOpen,
92
111
  onOpenChange,
93
- backdrop = true,
94
- closeOnBackdropClick = true,
95
- closeOnEscape = true,
96
- className = '',
97
- style,
112
+ backdrop,
113
+ closeOnBackdropClick,
114
+ closeOnEscape,
98
115
  glass,
99
- }: EdgePanelProps) => {
100
- const {
101
- isOpen: isOpenState,
102
- containerRef,
103
- backdropRef,
104
- generateEdgePanelClass,
105
- closePanel,
106
- handleBackdropClick,
107
- } = useEdgePanel({
108
- position,
109
- mode,
110
- isOpen,
111
- onOpenChange,
112
- backdrop,
113
- closeOnBackdropClick,
114
- closeOnEscape,
115
- glass,
116
- });
117
-
118
- // Moved useRef outside of conditional rendering to fix hook order issue
119
- const glassContentRef = useRef<HTMLDivElement>(null);
120
-
121
- const panelClass = generateEdgePanelClass({
122
- position,
123
- isOpen,
124
- className: glass ? `${className} c-edge-panel--glass` : className,
125
- });
126
-
127
- // If not open and not controlled by parent, don't render
128
- // Note: useEdgePanel manages internal state if onOpenChange is not provided?
129
- // Looking at useEdgePanel (implied): it seems to return isOpenState.
130
- // If we return null here, animations might be cut off.
131
- // Usually EdgePanel/Drawer should stay mounted but hidden or conditionally mounted.
132
- // The original code returned null if !isOpenState && isOpen === false.
133
- // Let's keep that logic.
134
- if (!isOpenState && isOpen === false) {
135
- return null;
136
- }
137
-
138
- const defaultGlassProps = {
139
- elasticity: 0,
140
- };
141
-
142
- const glassProps = glass === true ? defaultGlassProps : { ...defaultGlassProps, ...glass };
143
-
144
- // Check for compound components
145
- const hasCompoundComponents = React.Children.toArray(children).some((child) =>
116
+ });
117
+
118
+ // Moved useRef outside of conditional rendering to fix hook order issue
119
+ const glassContentRef = useRef<HTMLDivElement>(null);
120
+
121
+ const panelClass = generateEdgePanelClass({
122
+ position,
123
+ isOpen,
124
+ className: glass ? `${className} c-edge-panel--glass` : className,
125
+ });
126
+
127
+ // If not open and not controlled by parent, don't render
128
+ // Note: useEdgePanel manages internal state if onOpenChange is not provided?
129
+ // Looking at useEdgePanel (implied): it seems to return isOpenState.
130
+ // If we return null here, animations might be cut off.
131
+ // Usually EdgePanel/Drawer should stay mounted but hidden or conditionally mounted.
132
+ // The original code returned null if !isOpenState && isOpen === false.
133
+ // Let's keep that logic.
134
+ if (!isOpenState && isOpen === false) {
135
+ return null;
136
+ }
137
+
138
+ const defaultGlassProps = {
139
+ elasticity: 0,
140
+ };
141
+
142
+ const glassProps = glass === true ? defaultGlassProps : { ...defaultGlassProps, ...glass };
143
+
144
+ // Check for compound components
145
+ const hasCompoundComponents = React.Children.toArray(children).some(
146
+ child =>
146
147
  React.isValidElement(child) &&
147
- ['EdgePanelHeader', 'EdgePanelBody', 'EdgePanelFooter'].includes((child.type as any).displayName)
148
- );
149
-
150
- const panelContent = hasCompoundComponents ? (
151
- children
152
- ) : (
153
- <>
154
- <div className="c-edge-panel__header">
155
- <h4>{title}</h4>
156
- <button
157
- className="c-edge-panel__close c-btn c-btn--icon"
158
- onClick={() => closePanel()}
159
- aria-label="Close panel"
160
- >
161
- <Icon name="X" />
162
- </button>
163
- </div>
164
- <div className="c-edge-panel__body">{children}</div>
165
- </>
166
- );
167
-
168
- return (
169
- <div className={panelClass} data-position={position} data-mode={mode} style={style}>
170
- {backdrop && (
171
- <div ref={backdropRef} className="c-edge-panel__backdrop" onClick={handleBackdropClick} />
148
+ ['EdgePanelHeader', 'EdgePanelBody', 'EdgePanelFooter'].includes(
149
+ (child.type as any).displayName
150
+ )
151
+ );
152
+
153
+ const panelContent = hasCompoundComponents ? (
154
+ children
155
+ ) : (
156
+ <>
157
+ <div className="c-edge-panel__header">
158
+ <h4>{title}</h4>
159
+ <button
160
+ className="c-edge-panel__close c-btn c-btn--icon"
161
+ onClick={() => closePanel()}
162
+ aria-label="Close panel"
163
+ >
164
+ <Icon name="X" />
165
+ </button>
166
+ </div>
167
+ <div className="c-edge-panel__body">{children}</div>
168
+ </>
169
+ );
170
+
171
+ return (
172
+ <div className={panelClass} data-position={position} data-mode={mode} style={style}>
173
+ {backdrop && (
174
+ <div ref={backdropRef} className="c-edge-panel__backdrop" onClick={handleBackdropClick} />
175
+ )}
176
+ <div ref={containerRef} className="c-edge-panel__container">
177
+ {glass ? (
178
+ <AtomixGlass {...glassProps}>
179
+ <div
180
+ ref={glassContentRef}
181
+ className="c-edge-panel__glass-content"
182
+ style={{ borderRadius: containerRef.current?.style.borderRadius }}
183
+ >
184
+ {panelContent}
185
+ </div>
186
+ </AtomixGlass>
187
+ ) : (
188
+ panelContent
172
189
  )}
173
- <div ref={containerRef} className="c-edge-panel__container">
174
- {glass ? (
175
- <AtomixGlass {...glassProps}>
176
- <div
177
- ref={glassContentRef}
178
- className="c-edge-panel__glass-content"
179
- style={{ borderRadius: containerRef.current?.style.borderRadius }}
180
- >
181
- {panelContent}
182
- </div>
183
- </AtomixGlass>
184
- ) : (
185
- panelContent
186
- )}
187
- </div>
188
190
  </div>
189
- );
190
- }
191
- ) as unknown as EdgePanelComponent;
191
+ </div>
192
+ );
193
+ };
194
+
195
+ export const EdgePanel = memo(EdgePanelComponentBase) as unknown as EdgePanelComponent;
192
196
 
193
197
  EdgePanel.displayName = 'EdgePanel';
194
198
  EdgePanel.Header = EdgePanelHeader;
@@ -12,8 +12,7 @@ export type SelectComponent = React.FC<SelectProps> & {
12
12
  /**
13
13
  * Select - A component for dropdown selection
14
14
  */
15
- export const Select: SelectComponent = memo(
16
- ({
15
+ const SelectComponentBase = ({
17
16
  options,
18
17
  value,
19
18
  onChange,
@@ -314,8 +313,9 @@ export const Select: SelectComponent = memo(
314
313
  }
315
314
 
316
315
  return selectContent;
317
- }
318
- ) as unknown as SelectComponent;
316
+ };
317
+
318
+ export const Select = memo(SelectComponentBase) as unknown as SelectComponent;
319
319
 
320
320
  export type { SelectProps };
321
321
 
@@ -9,8 +9,7 @@ export type ListComponent = React.FC<ListProps> & {
9
9
  Item: typeof ListItem;
10
10
  };
11
11
 
12
- export const List: ListComponent = memo(
13
- ({ children, variant = 'default', className = '', style, ...props }: ListProps) => {
12
+ const ListComponentBase = ({ children, variant = 'default', className = '', style, ...props }: ListProps) => {
14
13
  // Generate CSS classes
15
14
  const listClasses = [LIST.BASE_CLASS, variant !== 'default' && `c-list--${variant}`, className]
16
15
  .filter(Boolean)
@@ -34,8 +33,9 @@ export const List: ListComponent = memo(
34
33
  })}
35
34
  </ListElement>
36
35
  );
37
- }
38
- ) as unknown as ListComponent;
36
+ };
37
+
38
+ export const List = memo(ListComponentBase) as unknown as ListComponent;
39
39
 
40
40
  List.displayName = 'List';
41
41
  List.Item = ListItem;
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect, useRef, forwardRef } from 'react';
1
+ import React, { useState, useEffect, useRef, forwardRef, useCallback } from 'react';
2
2
  import { SideMenuProps } from '../../../lib/types/components';
3
3
  import { useSideMenu } from '../../../lib/composables/useSideMenu';
4
4
  import { Icon } from '../../Icon';
@@ -116,16 +116,16 @@ export const SideMenu = forwardRef<HTMLDivElement, SideMenuProps>(
116
116
  delete nestedInnerRefs.current[index];
117
117
  }
118
118
  });
119
- }, [menuItems?.length]);
119
+ }, [menuItems]);
120
120
 
121
121
  // Helper function to update nested wrapper height
122
- const updateNestedHeight = (index: number, isOpen: boolean) => {
122
+ const updateNestedHeight = useCallback((index: number, isOpen: boolean) => {
123
123
  const wrapper = nestedWrapperRefs.current[index];
124
124
  const inner = nestedInnerRefs.current[index];
125
125
  if (wrapper && inner) {
126
126
  wrapper.style.height = isOpen ? `${inner.scrollHeight}px` : '0px';
127
127
  }
128
- };
128
+ }, []);
129
129
 
130
130
  // Set initial heights for nested wrappers on mount and when menuItems change
131
131
  useEffect(() => {
@@ -141,7 +141,7 @@ export const SideMenu = forwardRef<HTMLDivElement, SideMenuProps>(
141
141
  return () => clearTimeout(timeoutId);
142
142
  // Only run when menuItems change, nestedItemStates is read but not in deps to avoid loops
143
143
  // eslint-disable-next-line react-hooks/exhaustive-deps
144
- }, [menuItems?.length]);
144
+ }, [menuItems, updateNestedHeight]);
145
145
 
146
146
  // Update nested wrapper heights when state changes
147
147
  useEffect(() => {
@@ -162,7 +162,7 @@ export const SideMenu = forwardRef<HTMLDivElement, SideMenuProps>(
162
162
  return () => {
163
163
  frameIds.forEach(id => cancelAnimationFrame(id));
164
164
  };
165
- }, [nestedItemStates, menuItems?.length]);
165
+ }, [nestedItemStates, menuItems, updateNestedHeight]);
166
166
 
167
167
  // Combine refs using utility
168
168
  const combinedRef = useForkRef(sideMenuRef, ref);
@@ -5,7 +5,7 @@ import React, { useRef, useEffect, useState } from 'react';
5
5
  */
6
6
  export interface PhotoViewerImageProps {
7
7
  /** Ref to the image element */
8
- imageRef: React.RefObject<HTMLImageElement>;
8
+ imageRef: React.RefObject<HTMLImageElement | null>;
9
9
  /** Ref to the container element */
10
10
  containerRef?: React.RefObject<HTMLDivElement | null>;
11
11
  /** Image source URL */