msr-hooks 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1252 -0
- package/package.json +50 -0
- package/src/hooks/index.js +53 -0
- package/src/hooks/useAsyncEffect/index.js +1 -0
- package/src/hooks/useAsyncEffect/useAsyncEffect.d.ts +6 -0
- package/src/hooks/useAsyncEffect/useAsyncEffect.js +31 -0
- package/src/hooks/useChangeIconColor/index.js +1 -0
- package/src/hooks/useChangeIconColor/useChangeIconColor.d.ts +1 -0
- package/src/hooks/useChangeIconColor/useChangeIconColor.js +39 -0
- package/src/hooks/useClickOutsideObject/index.js +1 -0
- package/src/hooks/useClickOutsideObject/useClickOutsideObject.d.ts +8 -0
- package/src/hooks/useClickOutsideObject/useClickOutsideObject.js +28 -0
- package/src/hooks/useClipboard/index.js +1 -0
- package/src/hooks/useClipboard/useClipboard.d.ts +4 -0
- package/src/hooks/useClipboard/useClipboard.js +27 -0
- package/src/hooks/useDebounce/index.js +1 -0
- package/src/hooks/useDebounce/useDebounce.d.ts +1 -0
- package/src/hooks/useDebounce/useDebounce.js +18 -0
- package/src/hooks/useDeepCompareEffect/index.js +1 -0
- package/src/hooks/useDeepCompareEffect/useDeepCompareEffect.d.ts +3 -0
- package/src/hooks/useDeepCompareEffect/useDeepCompareEffect.js +40 -0
- package/src/hooks/useDocumentVisibility/index.js +1 -0
- package/src/hooks/useDocumentVisibility/useDocumentVisibility.d.ts +1 -0
- package/src/hooks/useDocumentVisibility/useDocumentVisibility.js +19 -0
- package/src/hooks/useEffectAfterMount/index.js +1 -0
- package/src/hooks/useEffectAfterMount/useEffectAfterMount.d.ts +4 -0
- package/src/hooks/useEffectAfterMount/useEffectAfterMount.js +20 -0
- package/src/hooks/useElementScrollProgress/index.js +1 -0
- package/src/hooks/useElementScrollProgress/useElementScrollProgress.d.ts +5 -0
- package/src/hooks/useElementScrollProgress/useElementScrollProgress.js +37 -0
- package/src/hooks/useEscapeKey/index.js +1 -0
- package/src/hooks/useEscapeKey/useEscapeKey.d.ts +1 -0
- package/src/hooks/useEscapeKey/useEscapeKey.js +22 -0
- package/src/hooks/useEventListener/index.js +1 -0
- package/src/hooks/useEventListener/useEventListener.d.ts +8 -0
- package/src/hooks/useEventListener/useEventListener.js +25 -0
- package/src/hooks/useFetch/index.js +1 -0
- package/src/hooks/useFetch/useFetch.d.ts +11 -0
- package/src/hooks/useFetch/useFetch.js +35 -0
- package/src/hooks/useHoverIntent/index.js +1 -0
- package/src/hooks/useHoverIntent/useHoverIntent.d.ts +6 -0
- package/src/hooks/useHoverIntent/useHoverIntent.js +81 -0
- package/src/hooks/useIntersectionObserver/index.js +1 -0
- package/src/hooks/useIntersectionObserver/useIntersectionObserver.d.ts +5 -0
- package/src/hooks/useIntersectionObserver/useIntersectionObserver.js +25 -0
- package/src/hooks/useInterval/index.js +1 -0
- package/src/hooks/useInterval/useInterval.d.ts +4 -0
- package/src/hooks/useInterval/useInterval.js +23 -0
- package/src/hooks/useIsomorphicLayoutEffect/index.js +1 -0
- package/src/hooks/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.d.ts +3 -0
- package/src/hooks/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.js +3 -0
- package/src/hooks/useKeyPressSequence/index.js +1 -0
- package/src/hooks/useKeyPressSequence/useKeyPressSequence.d.ts +5 -0
- package/src/hooks/useKeyPressSequence/useKeyPressSequence.js +52 -0
- package/src/hooks/useKeyboardNavigation/index.js +1 -0
- package/src/hooks/useKeyboardNavigation/useKeyboardNavigation.d.ts +10 -0
- package/src/hooks/useKeyboardNavigation/useKeyboardNavigation.js +50 -0
- package/src/hooks/useLocalStorage/index.js +1 -0
- package/src/hooks/useLocalStorage/useLocalStorage.d.ts +4 -0
- package/src/hooks/useLocalStorage/useLocalStorage.js +44 -0
- package/src/hooks/useLockBodyScroll/index.js +1 -0
- package/src/hooks/useLockBodyScroll/useLockBodyScroll.d.ts +1 -0
- package/src/hooks/useLockBodyScroll/useLockBodyScroll.js +25 -0
- package/src/hooks/useMediaQuery/index.js +1 -0
- package/src/hooks/useMediaQuery/useMediaQuery.d.ts +1 -0
- package/src/hooks/useMediaQuery/useMediaQuery.js +24 -0
- package/src/hooks/useNetworkStatus/index.js +1 -0
- package/src/hooks/useNetworkStatus/useNetworkStatus.d.ts +7 -0
- package/src/hooks/useNetworkStatus/useNetworkStatus.js +47 -0
- package/src/hooks/usePageLeave/index.js +1 -0
- package/src/hooks/usePageLeave/usePageLeave.d.ts +1 -0
- package/src/hooks/usePageLeave/usePageLeave.js +27 -0
- package/src/hooks/useParentWidth/index.js +1 -0
- package/src/hooks/useParentWidth/useParentWidth.d.ts +8 -0
- package/src/hooks/useParentWidth/useParentWidth.js +30 -0
- package/src/hooks/usePortal/index.js +1 -0
- package/src/hooks/usePortal/usePortal.d.ts +1 -0
- package/src/hooks/usePortal/usePortal.js +33 -0
- package/src/hooks/usePrefersReducedMotion/index.js +1 -0
- package/src/hooks/usePrefersReducedMotion/usePrefersReducedMotion.d.ts +1 -0
- package/src/hooks/usePrefersReducedMotion/usePrefersReducedMotion.js +24 -0
- package/src/hooks/usePreventZoom/index.js +1 -0
- package/src/hooks/usePreventZoom/usePreventZoom.d.ts +4 -0
- package/src/hooks/usePreventZoom/usePreventZoom.js +39 -0
- package/src/hooks/usePrevious/index.js +1 -0
- package/src/hooks/usePrevious/usePrevious.d.ts +1 -0
- package/src/hooks/usePrevious/usePrevious.js +16 -0
- package/src/hooks/useResize/index.js +1 -0
- package/src/hooks/useResize/useResize.d.ts +16 -0
- package/src/hooks/useResize/useResize.js +32 -0
- package/src/hooks/useSpringValue/index.js +1 -0
- package/src/hooks/useSpringValue/useSpringValue.d.ts +4 -0
- package/src/hooks/useSpringValue/useSpringValue.js +66 -0
- package/src/hooks/useStateHistory/index.js +1 -0
- package/src/hooks/useStateHistory/useStateHistory.d.ts +17 -0
- package/src/hooks/useStateHistory/useStateHistory.js +70 -0
- package/src/hooks/useThrottle/index.js +1 -0
- package/src/hooks/useThrottle/useThrottle.d.ts +1 -0
- package/src/hooks/useThrottle/useThrottle.js +25 -0
- package/src/hooks/useTimeout/index.js +1 -0
- package/src/hooks/useTimeout/useTimeout.d.ts +4 -0
- package/src/hooks/useTimeout/useTimeout.js +22 -0
- package/src/hooks/useToggle/index.js +1 -0
- package/src/hooks/useToggle/useToggle.d.ts +3 -0
- package/src/hooks/useToggle/useToggle.js +16 -0
- package/src/hooks/useUndoRedo/index.js +1 -0
- package/src/hooks/useUndoRedo/useUndoRedo.d.ts +18 -0
- package/src/hooks/useUndoRedo/useUndoRedo.js +49 -0
- package/src/hooks/useWindowSize/index.js +1 -0
- package/src/hooks/useWindowSize/useWindowSize.d.ts +1 -0
- package/src/hooks/useWindowSize/useWindowSize.js +29 -0
- package/src/index.d.ts +1 -0
- package/src/index.js +1 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Detect deliberate hover with configurable delay and sensitivity.
|
|
5
|
+
* @param {React.RefObject<HTMLElement>} ref
|
|
6
|
+
* @param {{ delay?: number; leaveDelay?: number; sensitivity?: number }} options
|
|
7
|
+
* @returns {boolean}
|
|
8
|
+
*/
|
|
9
|
+
export function useHoverIntent(ref, options = {}) {
|
|
10
|
+
const { delay = 120, leaveDelay = 80, sensitivity = 8 } = options;
|
|
11
|
+
const [hovered, setHovered] = useState(false);
|
|
12
|
+
const startPos = useRef({ x: 0, y: 0 });
|
|
13
|
+
const lastPos = useRef({ x: 0, y: 0 });
|
|
14
|
+
const enterTimer = useRef();
|
|
15
|
+
const leaveTimer = useRef();
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const node = ref?.current;
|
|
19
|
+
if (!node) return undefined;
|
|
20
|
+
|
|
21
|
+
const clearEnter = () => {
|
|
22
|
+
if (enterTimer.current) {
|
|
23
|
+
clearTimeout(enterTimer.current);
|
|
24
|
+
enterTimer.current = undefined;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const clearLeave = () => {
|
|
29
|
+
if (leaveTimer.current) {
|
|
30
|
+
clearTimeout(leaveTimer.current);
|
|
31
|
+
leaveTimer.current = undefined;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const onMouseEnter = (event) => {
|
|
36
|
+
clearLeave();
|
|
37
|
+
startPos.current = { x: event.clientX, y: event.clientY };
|
|
38
|
+
lastPos.current = startPos.current;
|
|
39
|
+
|
|
40
|
+
const checkIntent = () => {
|
|
41
|
+
const dx = lastPos.current.x - startPos.current.x;
|
|
42
|
+
const dy = lastPos.current.y - startPos.current.y;
|
|
43
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
44
|
+
|
|
45
|
+
if (distance <= sensitivity) {
|
|
46
|
+
setHovered(true);
|
|
47
|
+
} else {
|
|
48
|
+
startPos.current = { ...lastPos.current };
|
|
49
|
+
enterTimer.current = setTimeout(checkIntent, delay);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
clearEnter();
|
|
54
|
+
enterTimer.current = setTimeout(checkIntent, delay);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const onMouseMove = (event) => {
|
|
58
|
+
lastPos.current = { x: event.clientX, y: event.clientY };
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const onMouseLeave = () => {
|
|
62
|
+
clearEnter();
|
|
63
|
+
clearLeave();
|
|
64
|
+
leaveTimer.current = setTimeout(() => setHovered(false), leaveDelay);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
node.addEventListener('mouseenter', onMouseEnter);
|
|
68
|
+
node.addEventListener('mousemove', onMouseMove);
|
|
69
|
+
node.addEventListener('mouseleave', onMouseLeave);
|
|
70
|
+
|
|
71
|
+
return () => {
|
|
72
|
+
clearEnter();
|
|
73
|
+
clearLeave();
|
|
74
|
+
node.removeEventListener('mouseenter', onMouseEnter);
|
|
75
|
+
node.removeEventListener('mousemove', onMouseMove);
|
|
76
|
+
node.removeEventListener('mouseleave', onMouseLeave);
|
|
77
|
+
};
|
|
78
|
+
}, [ref, delay, leaveDelay, sensitivity]);
|
|
79
|
+
|
|
80
|
+
return hovered;
|
|
81
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useIntersectionObserver } from './useIntersectionObserver';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Detect when element enters/leaves viewport.
|
|
5
|
+
* @param {object} options - IntersectionObserver options
|
|
6
|
+
* @returns {[React.RefObject, boolean]} [ref to attach, isIntersecting state]
|
|
7
|
+
*/
|
|
8
|
+
export function useIntersectionObserver(options = {}) {
|
|
9
|
+
const [isIntersecting, setIsIntersecting] = useState(false);
|
|
10
|
+
const targetRef = useRef(null);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const target = targetRef.current;
|
|
14
|
+
if (!target) return undefined;
|
|
15
|
+
|
|
16
|
+
const observer = new IntersectionObserver(([entry]) => {
|
|
17
|
+
setIsIntersecting(entry.isIntersecting);
|
|
18
|
+
}, options);
|
|
19
|
+
|
|
20
|
+
observer.observe(target);
|
|
21
|
+
return () => observer.disconnect();
|
|
22
|
+
}, [options]);
|
|
23
|
+
|
|
24
|
+
return [targetRef, isIntersecting];
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useInterval } from './useInterval';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Declarative setInterval with cleanup.
|
|
5
|
+
* @param {Function} callback - Function to call on interval
|
|
6
|
+
* @param {number|null} delay - Delay in ms, null to pause
|
|
7
|
+
* @returns {void}
|
|
8
|
+
*/
|
|
9
|
+
export function useInterval(callback, delay) {
|
|
10
|
+
const savedCallback = useRef();
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
savedCallback.current = callback;
|
|
14
|
+
}, [callback]);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (delay === null) return undefined;
|
|
18
|
+
|
|
19
|
+
const tick = () => savedCallback.current();
|
|
20
|
+
const id = setInterval(tick, delay);
|
|
21
|
+
return () => clearInterval(id);
|
|
22
|
+
}, [delay]);
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useKeyPressSequence } from './useKeyPressSequence';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Run a handler when a key sequence is pressed in order.
|
|
5
|
+
* @param {string | string[]} sequence
|
|
6
|
+
* @param {(event: KeyboardEvent) => void} handler
|
|
7
|
+
* @param {{ timeout?: number }} options
|
|
8
|
+
*/
|
|
9
|
+
export function useKeyPressSequence(sequence, handler, options = {}) {
|
|
10
|
+
const { timeout = 1000 } = options;
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (!sequence || typeof window === 'undefined') return undefined;
|
|
14
|
+
const target = Array.isArray(sequence) ? sequence : String(sequence).split('');
|
|
15
|
+
let buffer = [];
|
|
16
|
+
let timerId;
|
|
17
|
+
|
|
18
|
+
const reset = () => {
|
|
19
|
+
buffer = [];
|
|
20
|
+
if (timerId) {
|
|
21
|
+
clearTimeout(timerId);
|
|
22
|
+
timerId = undefined;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const scheduleReset = () => {
|
|
27
|
+
if (!timeout) return;
|
|
28
|
+
if (timerId) clearTimeout(timerId);
|
|
29
|
+
timerId = setTimeout(reset, timeout);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const handleKeyDown = (event) => {
|
|
33
|
+
buffer.push(event.key);
|
|
34
|
+
if (buffer.length > target.length) {
|
|
35
|
+
buffer.shift();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
scheduleReset();
|
|
39
|
+
|
|
40
|
+
if (buffer.length === target.length && target.every((key, idx) => buffer[idx] === key)) {
|
|
41
|
+
handler?.(event);
|
|
42
|
+
reset();
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
47
|
+
return () => {
|
|
48
|
+
window.removeEventListener('keydown', handleKeyDown);
|
|
49
|
+
if (timerId) clearTimeout(timerId);
|
|
50
|
+
};
|
|
51
|
+
}, [sequence, handler, timeout]);
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useKeyboardNavigation } from './useKeyboardNavigation';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface UseKeyboardNavigationConfig {
|
|
2
|
+
selectedIndex: number | null;
|
|
3
|
+
handleSelect: (index: number | null) => void;
|
|
4
|
+
totalBytes: number;
|
|
5
|
+
bytesPerRow: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function useKeyboardNavigation(
|
|
9
|
+
config: UseKeyboardNavigationConfig
|
|
10
|
+
): void;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Navigate through grid-like structures with arrow keys.
|
|
5
|
+
* @param {object} config - Configuration object
|
|
6
|
+
* @param {number} config.selectedIndex - Currently selected item index
|
|
7
|
+
* @param {Function} config.handleSelect - Callback to update selection
|
|
8
|
+
* @param {number} config.totalBytes - Total number of items
|
|
9
|
+
* @param {number} config.bytesPerRow - Items per row in grid
|
|
10
|
+
* @returns {void}
|
|
11
|
+
*/
|
|
12
|
+
export function useKeyboardNavigation({
|
|
13
|
+
selectedIndex,
|
|
14
|
+
handleSelect,
|
|
15
|
+
totalBytes,
|
|
16
|
+
bytesPerRow
|
|
17
|
+
}) {
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const handleKeyDown = (event) => {
|
|
20
|
+
if (selectedIndex === null) return;
|
|
21
|
+
|
|
22
|
+
let newIndex = selectedIndex;
|
|
23
|
+
|
|
24
|
+
switch (event.key) {
|
|
25
|
+
case 'ArrowLeft':
|
|
26
|
+
newIndex = Math.max(0, selectedIndex - 1);
|
|
27
|
+
break;
|
|
28
|
+
case 'ArrowRight':
|
|
29
|
+
newIndex = Math.min(totalBytes - 1, selectedIndex + 1);
|
|
30
|
+
break;
|
|
31
|
+
case 'ArrowUp':
|
|
32
|
+
newIndex = Math.max(0, selectedIndex - bytesPerRow);
|
|
33
|
+
break;
|
|
34
|
+
case 'ArrowDown':
|
|
35
|
+
newIndex = Math.min(totalBytes - 1, selectedIndex + bytesPerRow);
|
|
36
|
+
break;
|
|
37
|
+
default:
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (newIndex !== selectedIndex) {
|
|
42
|
+
event.preventDefault();
|
|
43
|
+
handleSelect(newIndex);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
48
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
49
|
+
}, [selectedIndex, handleSelect, totalBytes, bytesPerRow]);
|
|
50
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useLocalStorage } from './useLocalStorage';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* State synced to localStorage with JSON parsing and SSR guard.
|
|
5
|
+
* @param {string} key - Storage key
|
|
6
|
+
* @param {*} initialValue - Initial value if not in storage
|
|
7
|
+
* @returns {[*, Function]} [storedValue, setValue]
|
|
8
|
+
*/
|
|
9
|
+
export function useLocalStorage(key, initialValue) {
|
|
10
|
+
const isBrowser = typeof window !== 'undefined';
|
|
11
|
+
|
|
12
|
+
const readValue = useCallback(() => {
|
|
13
|
+
if (!isBrowser) return initialValue;
|
|
14
|
+
try {
|
|
15
|
+
const item = window.localStorage.getItem(key);
|
|
16
|
+
return item ? JSON.parse(item) : initialValue;
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.warn('useLocalStorage read error', error);
|
|
19
|
+
return initialValue;
|
|
20
|
+
}
|
|
21
|
+
}, [initialValue, isBrowser, key]);
|
|
22
|
+
|
|
23
|
+
const [storedValue, setStoredValue] = useState(readValue);
|
|
24
|
+
|
|
25
|
+
const setValue = useCallback(
|
|
26
|
+
(value) => {
|
|
27
|
+
if (!isBrowser) return;
|
|
28
|
+
try {
|
|
29
|
+
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
|
30
|
+
setStoredValue(valueToStore);
|
|
31
|
+
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.warn('useLocalStorage write error', error);
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
[isBrowser, key, storedValue]
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
setStoredValue(readValue());
|
|
41
|
+
}, [readValue]);
|
|
42
|
+
|
|
43
|
+
return [storedValue, setValue];
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useLockBodyScroll } from './useLockBodyScroll';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function useLockBodyScroll(enabled?: boolean): void;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lock document body scrolling while enabled.
|
|
5
|
+
* @param {boolean} enabled
|
|
6
|
+
*/
|
|
7
|
+
export function useLockBodyScroll(enabled = true) {
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
if (!enabled || typeof document === 'undefined') return undefined;
|
|
10
|
+
|
|
11
|
+
const originalOverflow = document.body.style.overflow;
|
|
12
|
+
const originalPaddingRight = document.body.style.paddingRight;
|
|
13
|
+
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|
|
14
|
+
|
|
15
|
+
document.body.style.overflow = 'hidden';
|
|
16
|
+
if (scrollbarWidth > 0) {
|
|
17
|
+
document.body.style.paddingRight = `${scrollbarWidth}px`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return () => {
|
|
21
|
+
document.body.style.overflow = originalOverflow;
|
|
22
|
+
document.body.style.paddingRight = originalPaddingRight;
|
|
23
|
+
};
|
|
24
|
+
}, [enabled]);
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useMediaQuery } from './useMediaQuery';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function useMediaQuery(query: string): boolean;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Detect if media query matches current viewport.
|
|
5
|
+
* @param {string} query - Media query string (e.g., '(min-width: 768px)')
|
|
6
|
+
* @returns {boolean} Whether the media query matches
|
|
7
|
+
*/
|
|
8
|
+
export function useMediaQuery(query) {
|
|
9
|
+
const [matches, setMatches] = useState(false);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (typeof window === 'undefined') return undefined;
|
|
13
|
+
|
|
14
|
+
const mediaQuery = window.matchMedia(query);
|
|
15
|
+
setMatches(mediaQuery.matches);
|
|
16
|
+
|
|
17
|
+
const handler = (event) => setMatches(event.matches);
|
|
18
|
+
|
|
19
|
+
mediaQuery.addEventListener('change', handler);
|
|
20
|
+
return () => mediaQuery.removeEventListener('change', handler);
|
|
21
|
+
}, [query]);
|
|
22
|
+
|
|
23
|
+
return matches;
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useNetworkStatus } from './useNetworkStatus';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
const getConnection = () => {
|
|
4
|
+
if (typeof navigator === 'undefined') return null;
|
|
5
|
+
return navigator.connection || navigator.mozConnection || navigator.webkitConnection || null;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const readStatus = () => {
|
|
9
|
+
const online = typeof navigator !== 'undefined' ? navigator.onLine : true;
|
|
10
|
+
const conn = getConnection();
|
|
11
|
+
return {
|
|
12
|
+
online,
|
|
13
|
+
downlink: conn?.downlink ?? null,
|
|
14
|
+
rtt: conn?.rtt ?? null,
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Detect network status with basic debouncing and connection details.
|
|
20
|
+
* @returns {{online: boolean, downlink: number|null, rtt: number|null}}
|
|
21
|
+
*/
|
|
22
|
+
export function useNetworkStatus() {
|
|
23
|
+
const [state, setState] = useState(readStatus);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
let timer = null;
|
|
27
|
+
const scheduleUpdate = () => {
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
timer = setTimeout(() => setState(readStatus()), 150);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
window.addEventListener('online', scheduleUpdate);
|
|
33
|
+
window.addEventListener('offline', scheduleUpdate);
|
|
34
|
+
|
|
35
|
+
const conn = getConnection();
|
|
36
|
+
conn?.addEventListener?.('change', scheduleUpdate);
|
|
37
|
+
|
|
38
|
+
return () => {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
window.removeEventListener('online', scheduleUpdate);
|
|
41
|
+
window.removeEventListener('offline', scheduleUpdate);
|
|
42
|
+
conn?.removeEventListener?.('change', scheduleUpdate);
|
|
43
|
+
};
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
return state;
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { usePageLeave } from './usePageLeave';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function usePageLeave(handler: () => void): void;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Trigger a callback when the user is about to leave the page/tab.
|
|
5
|
+
* @param {Function} handler - Callback to run before leave
|
|
6
|
+
*/
|
|
7
|
+
export function usePageLeave(handler) {
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const onBeforeUnload = (e) => {
|
|
10
|
+
handler?.();
|
|
11
|
+
};
|
|
12
|
+
const onVisibility = () => {
|
|
13
|
+
if (document.visibilityState === 'hidden') handler?.();
|
|
14
|
+
};
|
|
15
|
+
const onPageHide = () => handler?.();
|
|
16
|
+
|
|
17
|
+
window.addEventListener('beforeunload', onBeforeUnload);
|
|
18
|
+
document.addEventListener('visibilitychange', onVisibility);
|
|
19
|
+
window.addEventListener('pagehide', onPageHide);
|
|
20
|
+
|
|
21
|
+
return () => {
|
|
22
|
+
window.removeEventListener('beforeunload', onBeforeUnload);
|
|
23
|
+
document.removeEventListener('visibilitychange', onVisibility);
|
|
24
|
+
window.removeEventListener('pagehide', onPageHide);
|
|
25
|
+
};
|
|
26
|
+
}, [handler]);
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useParentWidth } from './useParentWidth';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Get the parent element's width and a ref for the child element.
|
|
5
|
+
* @returns {{parentWidth: number|null, childRef: React.RefObject}} Parent width and child ref
|
|
6
|
+
*/
|
|
7
|
+
export function useParentWidth() {
|
|
8
|
+
const [parentWidth, setParentWidth] = useState(null);
|
|
9
|
+
const childRef = useRef(null);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (!childRef.current || !childRef.current.parentElement) return;
|
|
13
|
+
|
|
14
|
+
const parentElement = childRef.current.parentElement;
|
|
15
|
+
|
|
16
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
|
17
|
+
entries.forEach((entry) => {
|
|
18
|
+
if (entry.target === parentElement) {
|
|
19
|
+
setParentWidth(entry.contentRect.width);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
resizeObserver.observe(parentElement);
|
|
25
|
+
|
|
26
|
+
return () => resizeObserver.disconnect();
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
return { parentWidth, childRef };
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { usePortal } from './usePortal';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function usePortal(id?: string): HTMLElement | null;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ensure a DOM node exists for portals and return it.
|
|
5
|
+
* @param {string} id
|
|
6
|
+
* @returns {HTMLElement | null}
|
|
7
|
+
*/
|
|
8
|
+
export function usePortal(id) {
|
|
9
|
+
const [el] = useState(() => {
|
|
10
|
+
if (typeof document === 'undefined') return null;
|
|
11
|
+
const existing = id ? document.getElementById(id) : null;
|
|
12
|
+
if (existing) return existing;
|
|
13
|
+
const created = document.createElement('div');
|
|
14
|
+
if (id) created.id = id;
|
|
15
|
+
return created;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!el || typeof document === 'undefined') return undefined;
|
|
20
|
+
const alreadyInDom = document.body.contains(el);
|
|
21
|
+
if (!alreadyInDom) {
|
|
22
|
+
document.body.appendChild(el);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return () => {
|
|
26
|
+
if (!alreadyInDom && el.parentNode) {
|
|
27
|
+
el.parentNode.removeChild(el);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}, [el]);
|
|
31
|
+
|
|
32
|
+
return el;
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { usePrefersReducedMotion } from './usePrefersReducedMotion';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function usePrefersReducedMotion(): boolean;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Detect OS reduced motion preference.
|
|
5
|
+
* @returns {boolean} true if user prefers reduced motion
|
|
6
|
+
*/
|
|
7
|
+
export function usePrefersReducedMotion() {
|
|
8
|
+
const isBrowser = typeof window !== 'undefined';
|
|
9
|
+
const query = '(prefers-reduced-motion: reduce)';
|
|
10
|
+
const [prefers, setPrefers] = useState(() => {
|
|
11
|
+
if (!isBrowser || !window.matchMedia) return false;
|
|
12
|
+
return window.matchMedia(query).matches;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (!isBrowser || !window.matchMedia) return undefined;
|
|
17
|
+
const mql = window.matchMedia(query);
|
|
18
|
+
const handler = (event) => setPrefers(event.matches);
|
|
19
|
+
mql.addEventListener('change', handler);
|
|
20
|
+
return () => mql.removeEventListener('change', handler);
|
|
21
|
+
}, [isBrowser]);
|
|
22
|
+
|
|
23
|
+
return prefers;
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { usePreventZoom } from './usePreventZoom';
|