@shohojdhara/atomix 0.5.1 → 0.5.4
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/atomix.config.ts +45 -33
- package/build-tools/webpack-loader.js +5 -4
- package/dist/atomix.css +138 -17
- package/dist/atomix.css.map +1 -1
- package/dist/atomix.min.css +1 -1
- package/dist/atomix.min.css.map +1 -1
- package/dist/build-tools/webpack-loader.js +5 -4
- package/dist/charts.d.ts +23 -23
- package/dist/charts.js +40 -37
- package/dist/charts.js.map +1 -1
- package/dist/config.d.ts +699 -0
- package/dist/config.js +17 -0
- package/dist/config.js.map +1 -0
- package/dist/core.d.ts +2 -2
- package/dist/core.js +111 -50
- package/dist/core.js.map +1 -1
- package/dist/forms.d.ts +3 -6
- package/dist/forms.js +2 -2
- package/dist/forms.js.map +1 -1
- package/dist/heavy.d.ts +1 -1
- package/dist/heavy.js +173 -111
- package/dist/heavy.js.map +1 -1
- package/dist/index.d.ts +1881 -790
- package/dist/index.esm.js +2713 -816
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +2693 -780
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/layout.js +59 -60
- package/dist/layout.js.map +1 -1
- package/dist/theme.d.ts +1390 -276
- package/dist/theme.js +2133 -625
- package/dist/theme.js.map +1 -1
- package/package.json +14 -9
- package/scripts/atomix-cli.js +15 -1
- package/scripts/cli/__tests__/complexity-utils.test.js +24 -0
- package/scripts/cli/__tests__/detector.test.js +50 -0
- package/scripts/cli/__tests__/template-engine.test.js +23 -0
- package/scripts/cli/__tests__/test-setup.js +3 -0
- package/scripts/cli/commands/doctor.js +15 -3
- package/scripts/cli/commands/generate.js +113 -51
- package/scripts/cli/internal/ai-engine.js +30 -10
- package/scripts/cli/internal/complexity-utils.js +60 -0
- package/scripts/cli/internal/component-validator.js +49 -16
- package/scripts/cli/internal/config-loader.js +30 -20
- package/scripts/cli/internal/generator.js +89 -36
- package/scripts/cli/internal/hook-generator.js +5 -2
- package/scripts/cli/internal/itcss-generator.js +16 -12
- package/scripts/cli/templates/next-templates.js +81 -30
- package/scripts/cli/templates/storybook-templates.js +12 -2
- package/scripts/cli/utils/detector.js +45 -7
- package/scripts/cli/utils/diagnostics.js +78 -0
- package/scripts/cli/utils/telemetry.js +13 -0
- package/src/components/Accordion/Accordion.stories.tsx +4 -0
- package/src/components/AtomixGlass/AtomixGlassContainer.tsx +1 -1
- package/src/components/AtomixGlass/__snapshots__/AtomixGlass.test.tsx.snap +219 -0
- package/src/components/AtomixGlass/glass-utils.ts +1 -1
- package/src/components/Button/Button.tsx +114 -57
- package/src/components/Callout/Callout.tsx +4 -4
- package/src/components/Chart/ChartRenderer.tsx +1 -1
- package/src/components/Chart/DonutChart.tsx +11 -8
- package/src/components/EdgePanel/EdgePanel.tsx +119 -115
- package/src/components/Form/Select.tsx +4 -4
- package/src/components/List/List.tsx +4 -4
- package/src/components/Navigation/SideMenu/SideMenu.tsx +6 -6
- package/src/components/PhotoViewer/PhotoViewerImage.tsx +1 -1
- package/src/components/ProductReview/ProductReview.tsx +4 -2
- package/src/components/Rating/Rating.tsx +4 -2
- package/src/components/SectionIntro/SectionIntro.tsx +4 -2
- package/src/components/Steps/Steps.tsx +1 -1
- package/src/components/Tabs/Tabs.tsx +5 -5
- package/src/components/Testimonial/Testimonial.tsx +4 -2
- package/src/components/VideoPlayer/VideoPlayer.tsx +4 -2
- package/src/layouts/CssGrid/CssGrid.stories.tsx +464 -0
- package/src/layouts/CssGrid/CssGrid.tsx +215 -0
- package/src/layouts/CssGrid/index.ts +8 -0
- package/src/layouts/CssGrid/scripts/CssGrid.js +284 -0
- package/src/layouts/CssGrid/scripts/index.js +43 -0
- package/src/layouts/Grid/scripts/Container.js +139 -0
- package/src/layouts/Grid/scripts/Grid.js +184 -0
- package/src/layouts/Grid/scripts/GridCol.js +273 -0
- package/src/layouts/Grid/scripts/Row.js +154 -0
- package/src/layouts/Grid/scripts/index.js +48 -0
- package/src/layouts/MasonryGrid/MasonryGrid.tsx +71 -59
- package/src/lib/composables/atomix-glass/useGlassSize.ts +1 -1
- package/src/lib/composables/useAccordion.ts +5 -5
- package/src/lib/composables/useAtomixGlass.ts +3 -3
- package/src/lib/composables/useBarChart.ts +2 -2
- package/src/lib/composables/useChart.ts +3 -2
- package/src/lib/composables/useChartToolbar.ts +48 -66
- package/src/lib/composables/useDataTable.ts +1 -1
- package/src/lib/composables/useDatePicker.ts +2 -2
- package/src/lib/composables/useEdgePanel.ts +45 -54
- package/src/lib/composables/useHeroBackgroundSlider.ts +5 -5
- package/src/lib/composables/usePhotoViewer.ts +2 -3
- package/src/lib/composables/usePieChart.ts +1 -1
- package/src/lib/composables/usePopover.ts +151 -139
- package/src/lib/composables/useSideMenu.ts +28 -41
- package/src/lib/composables/useSlider.ts +2 -6
- package/src/lib/composables/useTooltip.ts +2 -2
- package/src/lib/config/index.ts +38 -323
- package/src/lib/config/loader.ts +419 -0
- package/src/lib/config/public-api.ts +43 -0
- package/src/lib/config/types.ts +389 -0
- package/src/lib/config/validator.ts +305 -0
- package/src/lib/theme/adapters/index.ts +1 -1
- package/src/lib/theme/adapters/themeAdapter.ts +358 -229
- package/src/lib/theme/components/ThemeToggle.tsx +276 -0
- package/src/lib/theme/config/configLoader.ts +351 -0
- package/src/lib/theme/config/loader.ts +221 -0
- package/src/lib/theme/core/createTheme.ts +126 -50
- package/src/lib/theme/core/createThemeObject.ts +7 -4
- package/src/lib/theme/devtools/Comparator.tsx +1 -1
- package/src/lib/theme/devtools/Inspector.tsx +1 -1
- package/src/lib/theme/devtools/LiveEditor.tsx +1 -1
- package/src/lib/theme/hooks/useThemeSwitcher.ts +164 -0
- package/src/lib/theme/index.ts +322 -38
- package/src/lib/theme/runtime/ThemeProvider.tsx +45 -11
- package/src/lib/theme/runtime/__tests__/ThemeProvider.test.tsx +44 -393
- package/src/lib/theme/runtime/useTheme.ts +1 -0
- package/src/lib/theme/tokens/tokens.ts +101 -1
- package/src/lib/theme/types.ts +91 -0
- package/src/lib/theme/utils/performanceMonitor.ts +315 -0
- package/src/lib/theme/utils/responsive.ts +280 -0
- package/src/lib/theme/utils/themeUtils.ts +531 -117
- package/src/styles/01-settings/_index.scss +1 -0
- package/src/styles/01-settings/_settings.atomix-glass.scss +174 -0
- package/src/styles/01-settings/_settings.masonry-grid.scss +42 -6
- package/src/styles/02-tools/_tools.glass.scss +6 -0
- package/src/styles/05-objects/_objects.masonry-grid.scss +162 -24
- package/src/styles/06-components/_components.atomix-glass.scss +4 -4
- package/src/lib/composables/useBreadcrumb.ts +0 -81
- package/src/lib/composables/useChartInteractions.ts +0 -123
- package/src/lib/composables/useChartPerformance.ts +0 -347
- package/src/lib/composables/useDropdown.ts +0 -338
- package/src/lib/composables/useModal.ts +0 -110
- package/src/lib/hooks/usePerformanceMonitor.ts +0 -148
- package/src/lib/utils/displacement-generator.ts +0 -92
- package/src/lib/utils/memoryMonitor.ts +0 -191
- package/src/styles/01-settings/_settings.testtypecheck.scss +0 -53
- package/src/styles/01-settings/_settings.typedbutton.scss +0 -53
- package/src/styles/06-components/_components.testbutton.scss +0 -212
- package/src/styles/06-components/_components.testtypecheck.scss +0 -212
- package/src/styles/06-components/_components.typedbutton.scss +0 -212
|
@@ -8,19 +8,19 @@ import { EDGE_PANEL } from '../constants/components';
|
|
|
8
8
|
* @returns EdgePanel state and methods
|
|
9
9
|
*/
|
|
10
10
|
export function useEdgePanel(initialProps?: Partial<EdgePanelProps>) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const [isOpen, setIsOpen] = useState(
|
|
11
|
+
const {
|
|
12
|
+
position = 'start',
|
|
13
|
+
mode = 'slide',
|
|
14
|
+
isOpen: propIsOpen = false,
|
|
15
|
+
backdrop = true,
|
|
16
|
+
closeOnBackdropClick = true,
|
|
17
|
+
closeOnEscape = true,
|
|
18
|
+
glass,
|
|
19
|
+
onOpenChange,
|
|
20
|
+
className = '',
|
|
21
|
+
} = initialProps || {};
|
|
22
|
+
|
|
23
|
+
const [isOpen, setIsOpen] = useState(propIsOpen || false);
|
|
24
24
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
25
25
|
const backdropRef = useRef<HTMLDivElement>(null);
|
|
26
26
|
|
|
@@ -30,22 +30,21 @@ export function useEdgePanel(initialProps?: Partial<EdgePanelProps>) {
|
|
|
30
30
|
* @returns Class string
|
|
31
31
|
*/
|
|
32
32
|
const generateEdgePanelClass = (props: Partial<EdgePanelProps>): string => {
|
|
33
|
-
const { position =
|
|
33
|
+
const { position: propPosition = position, className: propClassName = className, isOpen: argIsOpen } = props;
|
|
34
34
|
|
|
35
35
|
const baseClass = EDGE_PANEL.CLASSES.BASE;
|
|
36
|
-
const positionClass =
|
|
37
|
-
const openClass = (
|
|
36
|
+
const positionClass = propPosition ? `${baseClass}--${propPosition}` : '';
|
|
37
|
+
const openClass = (argIsOpen ?? isOpen) ? EDGE_PANEL.CLASSES.IS_OPEN : '';
|
|
38
38
|
|
|
39
|
-
return `${baseClass} ${positionClass} ${openClass} ${
|
|
39
|
+
return `${baseClass} ${positionClass} ${openClass} ${propClassName}`.trim();
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
* Adjust body padding in push mode
|
|
44
44
|
*/
|
|
45
45
|
const adjustBodyPadding = useCallback(() => {
|
|
46
|
-
if (!containerRef.current ||
|
|
46
|
+
if (!containerRef.current || mode !== 'push') return;
|
|
47
47
|
|
|
48
|
-
const { position } = defaultProps;
|
|
49
48
|
const size =
|
|
50
49
|
position === 'top' || position === 'bottom'
|
|
51
50
|
? containerRef.current.clientHeight
|
|
@@ -67,15 +66,13 @@ export function useEdgePanel(initialProps?: Partial<EdgePanelProps>) {
|
|
|
67
66
|
|
|
68
67
|
document.body.style[paddingProperty as any] = `${size}px`;
|
|
69
68
|
document.body.classList.add('is-pushed');
|
|
70
|
-
}, [
|
|
69
|
+
}, [mode, position]);
|
|
71
70
|
|
|
72
71
|
/**
|
|
73
72
|
* Reset body padding
|
|
74
73
|
*/
|
|
75
74
|
const resetBodyPadding = useCallback(() => {
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
const { position } = defaultProps;
|
|
75
|
+
if (mode !== 'push') return;
|
|
79
76
|
|
|
80
77
|
// Map position to CSS padding property
|
|
81
78
|
let paddingProperty: string;
|
|
@@ -93,7 +90,7 @@ export function useEdgePanel(initialProps?: Partial<EdgePanelProps>) {
|
|
|
93
90
|
|
|
94
91
|
document.body.style[paddingProperty as any] = '';
|
|
95
92
|
document.body.classList.remove('is-pushed');
|
|
96
|
-
}, [
|
|
93
|
+
}, [mode, position]);
|
|
97
94
|
|
|
98
95
|
/**
|
|
99
96
|
* Open the panel
|
|
@@ -104,8 +101,6 @@ export function useEdgePanel(initialProps?: Partial<EdgePanelProps>) {
|
|
|
104
101
|
document.body.classList.add('is-edgepanel-open');
|
|
105
102
|
|
|
106
103
|
if (containerRef.current) {
|
|
107
|
-
const { mode } = defaultProps;
|
|
108
|
-
|
|
109
104
|
// Only add animation if not in 'none' mode
|
|
110
105
|
if (mode !== 'none') {
|
|
111
106
|
if (useFadeAnimation) {
|
|
@@ -148,16 +143,16 @@ export function useEdgePanel(initialProps?: Partial<EdgePanelProps>) {
|
|
|
148
143
|
}
|
|
149
144
|
|
|
150
145
|
// If push mode, adjust body padding
|
|
151
|
-
if (
|
|
146
|
+
if (mode === 'push') {
|
|
152
147
|
adjustBodyPadding();
|
|
153
148
|
}
|
|
154
149
|
}
|
|
155
150
|
|
|
156
|
-
if (
|
|
157
|
-
|
|
151
|
+
if (onOpenChange) {
|
|
152
|
+
onOpenChange(true);
|
|
158
153
|
}
|
|
159
154
|
},
|
|
160
|
-
[
|
|
155
|
+
[mode, adjustBodyPadding, onOpenChange]
|
|
161
156
|
);
|
|
162
157
|
|
|
163
158
|
/**
|
|
@@ -166,8 +161,6 @@ export function useEdgePanel(initialProps?: Partial<EdgePanelProps>) {
|
|
|
166
161
|
const closePanel = useCallback(
|
|
167
162
|
(useFadeAnimation = false) => {
|
|
168
163
|
if (containerRef.current) {
|
|
169
|
-
const { position, mode } = defaultProps;
|
|
170
|
-
|
|
171
164
|
// Only add animation if not in 'none' mode
|
|
172
165
|
if (mode !== 'none') {
|
|
173
166
|
if (useFadeAnimation) {
|
|
@@ -209,7 +202,7 @@ export function useEdgePanel(initialProps?: Partial<EdgePanelProps>) {
|
|
|
209
202
|
}
|
|
210
203
|
|
|
211
204
|
// Reset body padding if push mode
|
|
212
|
-
if (
|
|
205
|
+
if (mode === 'push') {
|
|
213
206
|
resetBodyPadding();
|
|
214
207
|
}
|
|
215
208
|
|
|
@@ -220,20 +213,20 @@ export function useEdgePanel(initialProps?: Partial<EdgePanelProps>) {
|
|
|
220
213
|
setIsOpen(false);
|
|
221
214
|
document.body.classList.remove('is-edgepanel-open');
|
|
222
215
|
|
|
223
|
-
if (
|
|
224
|
-
|
|
216
|
+
if (onOpenChange) {
|
|
217
|
+
onOpenChange(false);
|
|
225
218
|
}
|
|
226
219
|
}, hideDelay);
|
|
227
220
|
} else {
|
|
228
221
|
setIsOpen(false);
|
|
229
222
|
document.body.classList.remove('is-edgepanel-open');
|
|
230
223
|
|
|
231
|
-
if (
|
|
232
|
-
|
|
224
|
+
if (onOpenChange) {
|
|
225
|
+
onOpenChange(false);
|
|
233
226
|
}
|
|
234
227
|
}
|
|
235
228
|
},
|
|
236
|
-
[
|
|
229
|
+
[mode, position, onOpenChange, resetBodyPadding]
|
|
237
230
|
);
|
|
238
231
|
|
|
239
232
|
/**
|
|
@@ -241,11 +234,11 @@ export function useEdgePanel(initialProps?: Partial<EdgePanelProps>) {
|
|
|
241
234
|
*/
|
|
242
235
|
const handleEscapeKey = useCallback(
|
|
243
236
|
(event: KeyboardEvent) => {
|
|
244
|
-
if (
|
|
237
|
+
if (closeOnEscape && event.key === 'Escape' && isOpen) {
|
|
245
238
|
closePanel();
|
|
246
239
|
}
|
|
247
240
|
},
|
|
248
|
-
[closePanel,
|
|
241
|
+
[closePanel, closeOnEscape, isOpen]
|
|
249
242
|
);
|
|
250
243
|
|
|
251
244
|
/**
|
|
@@ -253,55 +246,53 @@ export function useEdgePanel(initialProps?: Partial<EdgePanelProps>) {
|
|
|
253
246
|
*/
|
|
254
247
|
const handleBackdropClick = useCallback(
|
|
255
248
|
(event: React.MouseEvent<HTMLDivElement>) => {
|
|
256
|
-
if (
|
|
249
|
+
if (closeOnBackdropClick && event.target === event.currentTarget) {
|
|
257
250
|
closePanel();
|
|
258
251
|
}
|
|
259
252
|
},
|
|
260
|
-
[closePanel,
|
|
253
|
+
[closePanel, closeOnBackdropClick]
|
|
261
254
|
);
|
|
262
255
|
|
|
263
256
|
/**
|
|
264
257
|
* Set up event listeners for keyboard events
|
|
265
258
|
*/
|
|
266
259
|
useEffect(() => {
|
|
267
|
-
if (isOpen &&
|
|
260
|
+
if (isOpen && closeOnEscape) {
|
|
268
261
|
document.addEventListener('keydown', handleEscapeKey);
|
|
269
262
|
}
|
|
270
263
|
|
|
271
264
|
return () => {
|
|
272
265
|
document.removeEventListener('keydown', handleEscapeKey);
|
|
273
266
|
};
|
|
274
|
-
}, [isOpen, handleEscapeKey,
|
|
267
|
+
}, [isOpen, handleEscapeKey, closeOnEscape]);
|
|
275
268
|
|
|
276
269
|
/**
|
|
277
270
|
* Set initial transform values
|
|
278
271
|
*/
|
|
279
272
|
useEffect(() => {
|
|
280
273
|
if (containerRef.current) {
|
|
281
|
-
const { position, mode } = defaultProps;
|
|
282
|
-
|
|
283
274
|
if (!isOpen && (mode === 'slide' || mode === 'push') && position) {
|
|
284
|
-
containerRef.current.style.transform = EDGE_PANEL.TRANSFORM_VALUES[position];
|
|
275
|
+
containerRef.current.style.transform = EDGE_PANEL.TRANSFORM_VALUES[position as keyof typeof EDGE_PANEL.TRANSFORM_VALUES];
|
|
285
276
|
// Set initial opacity for fade animations
|
|
286
|
-
if (
|
|
277
|
+
if (glass) {
|
|
287
278
|
containerRef.current.style.opacity = '0';
|
|
288
279
|
}
|
|
289
280
|
}
|
|
290
281
|
}
|
|
291
|
-
}, [
|
|
282
|
+
}, [mode, position, glass, isOpen]);
|
|
292
283
|
|
|
293
284
|
/**
|
|
294
285
|
* Sync with prop changes
|
|
295
286
|
*/
|
|
296
287
|
useEffect(() => {
|
|
297
|
-
if (
|
|
298
|
-
if (
|
|
299
|
-
openPanel(!!
|
|
288
|
+
if (propIsOpen !== undefined && propIsOpen !== isOpen) {
|
|
289
|
+
if (propIsOpen) {
|
|
290
|
+
openPanel(!!glass);
|
|
300
291
|
} else {
|
|
301
|
-
closePanel(!!
|
|
292
|
+
closePanel(!!glass);
|
|
302
293
|
}
|
|
303
294
|
}
|
|
304
|
-
}, [
|
|
295
|
+
}, [propIsOpen, closePanel, isOpen, openPanel, glass]);
|
|
305
296
|
|
|
306
297
|
return {
|
|
307
298
|
isOpen,
|
|
@@ -18,12 +18,12 @@ export interface UseHeroBackgroundSliderResult {
|
|
|
18
18
|
/**
|
|
19
19
|
* Array of refs for slide container elements
|
|
20
20
|
*/
|
|
21
|
-
slideRefs: React.RefObject<HTMLDivElement>[];
|
|
21
|
+
slideRefs: React.RefObject<HTMLDivElement | null>[];
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Array of refs for video elements
|
|
25
25
|
*/
|
|
26
|
-
videoRefs: React.RefObject<HTMLVideoElement>[];
|
|
26
|
+
videoRefs: React.RefObject<HTMLVideoElement | null>[];
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Handle slide transition to next index
|
|
@@ -55,18 +55,18 @@ export function useHeroBackgroundSlider(
|
|
|
55
55
|
const [isTransitioning, setIsTransitioning] = useState(false);
|
|
56
56
|
const autoplayRef = useRef<NodeJS.Timeout | null>(null);
|
|
57
57
|
const isPausedRef = useRef(false);
|
|
58
|
-
const callbackRef = useRef<() => void>();
|
|
58
|
+
const callbackRef = useRef<() => void | undefined>(undefined);
|
|
59
59
|
|
|
60
60
|
// Create refs for slide containers
|
|
61
61
|
const slideRefs = useMemo(
|
|
62
62
|
() => slides.map(() => React.createRef<HTMLDivElement>()),
|
|
63
|
-
[slides
|
|
63
|
+
[slides]
|
|
64
64
|
);
|
|
65
65
|
|
|
66
66
|
// Create refs for video elements
|
|
67
67
|
const videoRefs = useMemo(
|
|
68
68
|
() => slides.map(() => React.createRef<HTMLVideoElement>()),
|
|
69
|
-
[slides
|
|
69
|
+
[slides]
|
|
70
70
|
);
|
|
71
71
|
|
|
72
72
|
/**
|
|
@@ -305,7 +305,7 @@ export const usePhotoViewer = ({
|
|
|
305
305
|
};
|
|
306
306
|
});
|
|
307
307
|
},
|
|
308
|
-
[
|
|
308
|
+
[currentIndex, calculateBounds, constrainPosition]
|
|
309
309
|
);
|
|
310
310
|
|
|
311
311
|
const setImagePosition = useCallback(
|
|
@@ -366,7 +366,7 @@ export const usePhotoViewer = ({
|
|
|
366
366
|
};
|
|
367
367
|
});
|
|
368
368
|
},
|
|
369
|
-
[
|
|
369
|
+
[currentIndex, calculateBounds, constrainPosition]
|
|
370
370
|
);
|
|
371
371
|
|
|
372
372
|
// Handle mouse wheel for zooming with proper bounds
|
|
@@ -866,7 +866,6 @@ export const usePhotoViewer = ({
|
|
|
866
866
|
});
|
|
867
867
|
},
|
|
868
868
|
[
|
|
869
|
-
isMounted,
|
|
870
869
|
enableGestures,
|
|
871
870
|
isDragging,
|
|
872
871
|
startDragPosition,
|
|
@@ -294,7 +294,7 @@ export function usePieChart(data: ChartDataPoint[], options: PieChartOptions = {
|
|
|
294
294
|
|
|
295
295
|
return parts.join(' - ');
|
|
296
296
|
},
|
|
297
|
-
[options
|
|
297
|
+
[options]
|
|
298
298
|
);
|
|
299
299
|
|
|
300
300
|
// Get slice transform for hover effect
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useRef, useEffect, RefObject } from 'react';
|
|
1
|
+
import { useState, useRef, useEffect, RefObject, useCallback } from 'react';
|
|
2
2
|
|
|
3
3
|
type PopoverPosition = 'top' | 'bottom' | 'left' | 'right';
|
|
4
4
|
type PopoverTrigger = 'click' | 'hover';
|
|
@@ -57,14 +57,17 @@ export const usePopover = ({
|
|
|
57
57
|
const isOpenState = isControlled ? controlledIsOpen : isOpen;
|
|
58
58
|
|
|
59
59
|
// Define setIsOpen function before using it in useEffect
|
|
60
|
-
const setIsOpen = (
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
onOpenChange
|
|
66
|
-
|
|
67
|
-
|
|
60
|
+
const setIsOpen = useCallback(
|
|
61
|
+
(newIsOpen: boolean) => {
|
|
62
|
+
if (!isControlled) {
|
|
63
|
+
setIsOpenState(newIsOpen);
|
|
64
|
+
}
|
|
65
|
+
if (onOpenChange) {
|
|
66
|
+
onOpenChange(newIsOpen);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
[isControlled, onOpenChange]
|
|
70
|
+
);
|
|
68
71
|
|
|
69
72
|
// Handle hover events if trigger is hover
|
|
70
73
|
useEffect(() => {
|
|
@@ -115,141 +118,150 @@ export const usePopover = ({
|
|
|
115
118
|
popoverRef.current.addEventListener('mouseenter', handlePopoverMouseEnter);
|
|
116
119
|
popoverRef.current.addEventListener('mouseleave', handlePopoverMouseLeave);
|
|
117
120
|
|
|
121
|
+
const currentTrigger = triggerRef.current;
|
|
122
|
+
const currentPopover = popoverRef.current;
|
|
123
|
+
|
|
118
124
|
return () => {
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
|
|
125
|
+
if (currentTrigger) {
|
|
126
|
+
currentTrigger.removeEventListener('mouseenter', handleTriggerMouseEnter);
|
|
127
|
+
currentTrigger.removeEventListener('mouseleave', handleTriggerMouseLeave);
|
|
122
128
|
}
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
|
|
129
|
+
if (currentPopover) {
|
|
130
|
+
currentPopover.removeEventListener('mouseenter', handlePopoverMouseEnter);
|
|
131
|
+
currentPopover.removeEventListener('mouseleave', handlePopoverMouseLeave);
|
|
126
132
|
}
|
|
127
133
|
if (timeoutRef.current !== null) {
|
|
128
134
|
window.clearTimeout(timeoutRef.current);
|
|
129
135
|
}
|
|
130
136
|
};
|
|
131
|
-
}, [trigger, delay, isOpenState]);
|
|
132
|
-
|
|
133
|
-
const updatePosition = (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
// Calculate space available in each direction
|
|
154
|
-
const spaceTop = triggerRect.top;
|
|
155
|
-
const spaceBottom = viewportHeight - triggerRect.bottom;
|
|
156
|
-
const spaceLeft = triggerRect.left;
|
|
157
|
-
const spaceRight = viewportWidth - triggerRect.right;
|
|
158
|
-
|
|
159
|
-
// Determine best position based on available space
|
|
160
|
-
let bestPosition: PopoverPosition = position === 'auto' ? 'top' : (position as PopoverPosition);
|
|
161
|
-
|
|
162
|
-
// If specified position is 'auto', find the position with most space
|
|
163
|
-
if (position === 'auto') {
|
|
164
|
-
const spaces = [
|
|
165
|
-
{ position: 'top', space: spaceTop },
|
|
166
|
-
{ position: 'right', space: spaceRight },
|
|
167
|
-
{ position: 'bottom', space: spaceBottom },
|
|
168
|
-
{ position: 'left', space: spaceLeft },
|
|
169
|
-
];
|
|
170
|
-
|
|
171
|
-
// Sort by available space (descending)
|
|
172
|
-
spaces.sort((a, b) => b.space - a.space);
|
|
173
|
-
|
|
174
|
-
// Select position with most space
|
|
175
|
-
bestPosition = spaces[0]?.position as PopoverPosition;
|
|
176
|
-
} else {
|
|
177
|
-
// Check if the preferred position has enough space
|
|
178
|
-
const needsFlip =
|
|
179
|
-
(position === 'top' &&
|
|
180
|
-
spaceTop < popoverRect.height + offset &&
|
|
181
|
-
spaceBottom >= popoverRect.height + offset) ||
|
|
182
|
-
(position === 'bottom' &&
|
|
183
|
-
spaceBottom < popoverRect.height + offset &&
|
|
184
|
-
spaceTop >= popoverRect.height + offset) ||
|
|
185
|
-
(position === 'left' &&
|
|
186
|
-
spaceLeft < popoverRect.width + offset &&
|
|
187
|
-
spaceRight >= popoverRect.width + offset) ||
|
|
188
|
-
(position === 'right' &&
|
|
189
|
-
spaceRight < popoverRect.width + offset &&
|
|
190
|
-
spaceLeft >= popoverRect.width + offset);
|
|
191
|
-
|
|
192
|
-
if (needsFlip) {
|
|
193
|
-
// Flip to the opposite side
|
|
194
|
-
const oppositePositions: Record<PopoverPosition | 'auto', PopoverPosition> = {
|
|
195
|
-
top: 'bottom',
|
|
196
|
-
bottom: 'top',
|
|
197
|
-
left: 'right',
|
|
198
|
-
right: 'left',
|
|
199
|
-
auto: 'bottom',
|
|
200
|
-
};
|
|
201
|
-
bestPosition = oppositePositions[position as PopoverPosition | 'auto'];
|
|
137
|
+
}, [trigger, delay, isOpenState, setIsOpen]);
|
|
138
|
+
|
|
139
|
+
const updatePosition = useCallback(
|
|
140
|
+
(event?: Event) => {
|
|
141
|
+
if (!triggerRef.current || !popoverRef.current) return;
|
|
142
|
+
|
|
143
|
+
const triggerRect = triggerRef.current.getBoundingClientRect();
|
|
144
|
+
const popoverRect = popoverRef.current.getBoundingClientRect();
|
|
145
|
+
const viewportWidth = window.innerWidth;
|
|
146
|
+
const viewportHeight = window.innerHeight;
|
|
147
|
+
|
|
148
|
+
// Check if the trigger is near viewport edges
|
|
149
|
+
const isNearViewportEdge =
|
|
150
|
+
triggerRect.top < 50 ||
|
|
151
|
+
triggerRect.bottom > viewportHeight - 50 ||
|
|
152
|
+
triggerRect.left < 50 ||
|
|
153
|
+
triggerRect.right > viewportWidth - 50;
|
|
154
|
+
|
|
155
|
+
// If this is a scroll update and trigger isn't near edges, skip repositioning
|
|
156
|
+
if (event?.type === 'scroll' && !isNearViewportEdge) {
|
|
157
|
+
return;
|
|
202
158
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
159
|
+
|
|
160
|
+
// Calculate space available in each direction
|
|
161
|
+
const spaceTop = triggerRect.top;
|
|
162
|
+
const spaceBottom = viewportHeight - triggerRect.bottom;
|
|
163
|
+
const spaceLeft = triggerRect.left;
|
|
164
|
+
const spaceRight = viewportWidth - triggerRect.right;
|
|
165
|
+
|
|
166
|
+
// Determine best position based on available space
|
|
167
|
+
let bestPosition: PopoverPosition =
|
|
168
|
+
position === 'auto' ? 'top' : (position as PopoverPosition);
|
|
169
|
+
|
|
170
|
+
// If specified position is 'auto', find the position with most space
|
|
171
|
+
if (position === 'auto') {
|
|
172
|
+
const spaces = [
|
|
173
|
+
{ position: 'top', space: spaceTop },
|
|
174
|
+
{ position: 'right', space: spaceRight },
|
|
175
|
+
{ position: 'bottom', space: spaceBottom },
|
|
176
|
+
{ position: 'left', space: spaceLeft },
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
// Sort by available space (descending)
|
|
180
|
+
spaces.sort((a, b) => b.space - a.space);
|
|
181
|
+
|
|
182
|
+
// Select position with most space
|
|
183
|
+
bestPosition = spaces[0]?.position as PopoverPosition;
|
|
184
|
+
} else {
|
|
185
|
+
// Check if the preferred position has enough space
|
|
186
|
+
const needsFlip =
|
|
187
|
+
(position === 'top' &&
|
|
188
|
+
spaceTop < popoverRect.height + offset &&
|
|
189
|
+
spaceBottom >= popoverRect.height + offset) ||
|
|
190
|
+
(position === 'bottom' &&
|
|
191
|
+
spaceBottom < popoverRect.height + offset &&
|
|
192
|
+
spaceTop >= popoverRect.height + offset) ||
|
|
193
|
+
(position === 'left' &&
|
|
194
|
+
spaceLeft < popoverRect.width + offset &&
|
|
195
|
+
spaceRight >= popoverRect.width + offset) ||
|
|
196
|
+
(position === 'right' &&
|
|
197
|
+
spaceRight < popoverRect.width + offset &&
|
|
198
|
+
spaceLeft >= popoverRect.width + offset);
|
|
199
|
+
|
|
200
|
+
if (needsFlip) {
|
|
201
|
+
// Flip to the opposite side
|
|
202
|
+
const oppositePositions: Record<PopoverPosition | 'auto', PopoverPosition> = {
|
|
203
|
+
top: 'bottom',
|
|
204
|
+
bottom: 'top',
|
|
205
|
+
left: 'right',
|
|
206
|
+
right: 'left',
|
|
207
|
+
auto: 'bottom',
|
|
208
|
+
};
|
|
209
|
+
bestPosition = oppositePositions[position as PopoverPosition | 'auto'];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
setCurrentPosition(bestPosition);
|
|
214
|
+
|
|
215
|
+
// Calculate position based on the determined best position
|
|
216
|
+
let top = 0;
|
|
217
|
+
let left = 0;
|
|
218
|
+
|
|
219
|
+
// Calculate viewport-relative position
|
|
220
|
+
switch (bestPosition) {
|
|
221
|
+
case 'top':
|
|
222
|
+
top = triggerRect.top - popoverRect.height - offset;
|
|
223
|
+
left = triggerRect.left + triggerRect.width / 2 - popoverRect.width / 2;
|
|
224
|
+
break;
|
|
225
|
+
case 'bottom':
|
|
226
|
+
top = triggerRect.bottom + offset;
|
|
227
|
+
left = triggerRect.left + triggerRect.width / 2 - popoverRect.width / 2;
|
|
228
|
+
break;
|
|
229
|
+
case 'left':
|
|
230
|
+
top = triggerRect.top + triggerRect.height / 2 - popoverRect.height / 2;
|
|
231
|
+
left = triggerRect.left - popoverRect.width - offset;
|
|
232
|
+
break;
|
|
233
|
+
case 'right':
|
|
234
|
+
top = triggerRect.top + triggerRect.height / 2 - popoverRect.height / 2;
|
|
235
|
+
left = triggerRect.right + offset;
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Constrain to viewport boundaries
|
|
240
|
+
if (left < 0) {
|
|
241
|
+
left = 5;
|
|
242
|
+
} else if (left + popoverRect.width > viewportWidth) {
|
|
243
|
+
left = viewportWidth - popoverRect.width - 5;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (top < 0) {
|
|
247
|
+
top = 5;
|
|
248
|
+
} else if (top + popoverRect.height > viewportHeight) {
|
|
249
|
+
top = viewportHeight - popoverRect.height - 5;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Add scroll position to convert viewport coordinates to absolute position
|
|
253
|
+
const absoluteTop = top + window.scrollY;
|
|
254
|
+
const absoluteLeft = left + window.scrollX;
|
|
255
|
+
|
|
256
|
+
// Apply position using absolute positioning to follow when scrolling
|
|
257
|
+
if (popoverRef.current) {
|
|
258
|
+
popoverRef.current.style.position = 'absolute';
|
|
259
|
+
popoverRef.current.style.top = `${absoluteTop}px`;
|
|
260
|
+
popoverRef.current.style.left = `${absoluteLeft}px`;
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
[position, offset]
|
|
264
|
+
);
|
|
253
265
|
|
|
254
266
|
// Position the popover
|
|
255
267
|
useEffect(() => {
|
|
@@ -289,7 +301,7 @@ export const usePopover = ({
|
|
|
289
301
|
}
|
|
290
302
|
clearInterval(intervalId);
|
|
291
303
|
};
|
|
292
|
-
}, [isOpenState,
|
|
304
|
+
}, [isOpenState, updatePosition]);
|
|
293
305
|
|
|
294
306
|
// Handle click outside to close popover
|
|
295
307
|
useEffect(() => {
|
|
@@ -311,7 +323,7 @@ export const usePopover = ({
|
|
|
311
323
|
return () => {
|
|
312
324
|
document.removeEventListener('mousedown', handleClickOutside);
|
|
313
325
|
};
|
|
314
|
-
}, [isOpenState, closeOnClickOutside]);
|
|
326
|
+
}, [isOpenState, closeOnClickOutside, setIsOpen]);
|
|
315
327
|
|
|
316
328
|
// Handle escape key to close popover
|
|
317
329
|
useEffect(() => {
|
|
@@ -328,7 +340,7 @@ export const usePopover = ({
|
|
|
328
340
|
return () => {
|
|
329
341
|
document.removeEventListener('keydown', handleEscapeKey);
|
|
330
342
|
};
|
|
331
|
-
}, [isOpenState, closeOnEscape]);
|
|
343
|
+
}, [isOpenState, closeOnEscape, setIsOpen]);
|
|
332
344
|
|
|
333
345
|
// Clean up on unmount
|
|
334
346
|
useEffect(() => {
|