@dloizides/rn-web-hooks 1.0.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/CHANGELOG.md +12 -0
- package/README.md +31 -0
- package/dist/index.d.mts +46 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +159 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +152 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +72 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
Initial release. Extracted verbatim from the byte-identical `hooks/useOnlineStatus`,
|
|
6
|
+
`hooks/useEscapeKey`, `hooks/useHighContrast`, and `utils/debounce` modules in
|
|
7
|
+
`katalogos-web` and `erevna-web` (extract-on-second-use). The only change is
|
|
8
|
+
`debounce` now imports `isValueDefined` directly from `@dloizides/utils` instead of
|
|
9
|
+
a local re-export shim.
|
|
10
|
+
|
|
11
|
+
- `useOnlineStatus`, `useEscapeKey`, `useHighContrast`
|
|
12
|
+
- `debounce`, `throttle`, `useDebouncedCallback`
|
package/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# @dloizides/rn-web-hooks
|
|
2
|
+
|
|
3
|
+
Cross-platform (React Native + `react-native-web`) React hooks and rate-limiting
|
|
4
|
+
helpers, extracted from the byte-identical copies in `katalogos-web` and `erevna-web`.
|
|
5
|
+
|
|
6
|
+
| Export | What |
|
|
7
|
+
|---|---|
|
|
8
|
+
| `useOnlineStatus()` | `{ isOnline, isOffline }` via `navigator.onLine` + online/offline events (web); always online on native |
|
|
9
|
+
| `useEscapeKey(handler, enabled?)` | fires `handler` on Escape keydown (web only, no-op native) |
|
|
10
|
+
| `useHighContrast()` | tracks `prefers-contrast: more` (web only, `false` native) |
|
|
11
|
+
| `debounce(fn, wait?)` | plain debounced function |
|
|
12
|
+
| `throttle(fn, wait?)` | plain throttled function |
|
|
13
|
+
| `useDebouncedCallback(cb, wait)` | stable debounced callback with `.flush()` / `.cancel()`, flushes on unmount |
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
npm install @dloizides/rn-web-hooks
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Peers: `react` (>=18), `react-native` (>=0.79). Runtime dep: `@dloizides/utils`.
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { useOnlineStatus, useEscapeKey, useDebouncedCallback } from '@dloizides/rn-web-hooks';
|
|
27
|
+
|
|
28
|
+
const { isOffline } = useOnlineStatus();
|
|
29
|
+
useEscapeKey(() => setOpen(false), open);
|
|
30
|
+
const debouncedSave = useDebouncedCallback((v: string) => save(v), 300);
|
|
31
|
+
```
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
interface OnlineStatus {
|
|
2
|
+
/** Whether the browser currently has network connectivity. */
|
|
3
|
+
isOnline: boolean;
|
|
4
|
+
/** Whether the browser is currently offline. */
|
|
5
|
+
isOffline: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Tracks browser online/offline status via `navigator.onLine`
|
|
9
|
+
* and the `online`/`offline` window events.
|
|
10
|
+
*
|
|
11
|
+
* On native platforms, always reports as online (native apps
|
|
12
|
+
* handle connectivity differently via React Query).
|
|
13
|
+
*/
|
|
14
|
+
declare function useOnlineStatus(): OnlineStatus;
|
|
15
|
+
|
|
16
|
+
declare function useEscapeKey(handler: () => void, enabled?: boolean): void;
|
|
17
|
+
|
|
18
|
+
declare function useHighContrast(): boolean;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Debounce and throttle utilities for rate limiting function calls.
|
|
22
|
+
*/
|
|
23
|
+
declare function debounce<T extends (...args: readonly unknown[]) => unknown>(fn: T, wait?: number): (...args: Parameters<T>) => void;
|
|
24
|
+
/**
|
|
25
|
+
* Creates a throttled version of a function that only invokes at most once
|
|
26
|
+
* per every `wait` milliseconds.
|
|
27
|
+
*
|
|
28
|
+
* @param fn - The function to throttle
|
|
29
|
+
* @param wait - The number of milliseconds to wait between invocations (default: 300)
|
|
30
|
+
* @returns A throttled version of the function
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* const throttledUpdate = throttle(updatePosition, 100);
|
|
34
|
+
* // Rapid calls will only trigger updatePosition every 100ms
|
|
35
|
+
*/
|
|
36
|
+
declare function throttle<T extends (...args: readonly unknown[]) => unknown>(fn: T, wait?: number): (...args: Parameters<T>) => void;
|
|
37
|
+
interface DebouncedCallback<T extends (...args: readonly unknown[]) => unknown> {
|
|
38
|
+
(...args: Parameters<T>): void;
|
|
39
|
+
/** Fire the pending call now (if any) and clear it. No-op if nothing pending. */
|
|
40
|
+
flush: () => void;
|
|
41
|
+
/** Drop the pending call (if any) without firing it. No-op if nothing pending. */
|
|
42
|
+
cancel: () => void;
|
|
43
|
+
}
|
|
44
|
+
declare function useDebouncedCallback<T extends (...args: readonly unknown[]) => unknown>(callback: T, wait: number): DebouncedCallback<T>;
|
|
45
|
+
|
|
46
|
+
export { debounce, throttle, useDebouncedCallback, useEscapeKey, useHighContrast, useOnlineStatus };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
interface OnlineStatus {
|
|
2
|
+
/** Whether the browser currently has network connectivity. */
|
|
3
|
+
isOnline: boolean;
|
|
4
|
+
/** Whether the browser is currently offline. */
|
|
5
|
+
isOffline: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Tracks browser online/offline status via `navigator.onLine`
|
|
9
|
+
* and the `online`/`offline` window events.
|
|
10
|
+
*
|
|
11
|
+
* On native platforms, always reports as online (native apps
|
|
12
|
+
* handle connectivity differently via React Query).
|
|
13
|
+
*/
|
|
14
|
+
declare function useOnlineStatus(): OnlineStatus;
|
|
15
|
+
|
|
16
|
+
declare function useEscapeKey(handler: () => void, enabled?: boolean): void;
|
|
17
|
+
|
|
18
|
+
declare function useHighContrast(): boolean;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Debounce and throttle utilities for rate limiting function calls.
|
|
22
|
+
*/
|
|
23
|
+
declare function debounce<T extends (...args: readonly unknown[]) => unknown>(fn: T, wait?: number): (...args: Parameters<T>) => void;
|
|
24
|
+
/**
|
|
25
|
+
* Creates a throttled version of a function that only invokes at most once
|
|
26
|
+
* per every `wait` milliseconds.
|
|
27
|
+
*
|
|
28
|
+
* @param fn - The function to throttle
|
|
29
|
+
* @param wait - The number of milliseconds to wait between invocations (default: 300)
|
|
30
|
+
* @returns A throttled version of the function
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* const throttledUpdate = throttle(updatePosition, 100);
|
|
34
|
+
* // Rapid calls will only trigger updatePosition every 100ms
|
|
35
|
+
*/
|
|
36
|
+
declare function throttle<T extends (...args: readonly unknown[]) => unknown>(fn: T, wait?: number): (...args: Parameters<T>) => void;
|
|
37
|
+
interface DebouncedCallback<T extends (...args: readonly unknown[]) => unknown> {
|
|
38
|
+
(...args: Parameters<T>): void;
|
|
39
|
+
/** Fire the pending call now (if any) and clear it. No-op if nothing pending. */
|
|
40
|
+
flush: () => void;
|
|
41
|
+
/** Drop the pending call (if any) without firing it. No-op if nothing pending. */
|
|
42
|
+
cancel: () => void;
|
|
43
|
+
}
|
|
44
|
+
declare function useDebouncedCallback<T extends (...args: readonly unknown[]) => unknown>(callback: T, wait: number): DebouncedCallback<T>;
|
|
45
|
+
|
|
46
|
+
export { debounce, throttle, useDebouncedCallback, useEscapeKey, useHighContrast, useOnlineStatus };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var reactNative = require('react-native');
|
|
5
|
+
var utils = require('@dloizides/utils');
|
|
6
|
+
|
|
7
|
+
// src/useOnlineStatus.ts
|
|
8
|
+
function isWebPlatform() {
|
|
9
|
+
return reactNative.Platform.OS === "web";
|
|
10
|
+
}
|
|
11
|
+
function getBrowserOnlineState() {
|
|
12
|
+
if (typeof navigator === "undefined") return true;
|
|
13
|
+
return navigator.onLine;
|
|
14
|
+
}
|
|
15
|
+
function useOnlineStatus() {
|
|
16
|
+
const [isOnline, setIsOnline] = react.useState(() => {
|
|
17
|
+
if (!isWebPlatform()) return true;
|
|
18
|
+
return getBrowserOnlineState();
|
|
19
|
+
});
|
|
20
|
+
const handleOnline = react.useCallback(() => {
|
|
21
|
+
setIsOnline(true);
|
|
22
|
+
}, []);
|
|
23
|
+
const handleOffline = react.useCallback(() => {
|
|
24
|
+
setIsOnline(false);
|
|
25
|
+
}, []);
|
|
26
|
+
react.useEffect(() => {
|
|
27
|
+
if (!isWebPlatform()) return;
|
|
28
|
+
if (typeof window === "undefined") return;
|
|
29
|
+
window.addEventListener("online", handleOnline);
|
|
30
|
+
window.addEventListener("offline", handleOffline);
|
|
31
|
+
return () => {
|
|
32
|
+
window.removeEventListener("online", handleOnline);
|
|
33
|
+
window.removeEventListener("offline", handleOffline);
|
|
34
|
+
};
|
|
35
|
+
}, [handleOnline, handleOffline]);
|
|
36
|
+
return { isOnline, isOffline: !isOnline };
|
|
37
|
+
}
|
|
38
|
+
function useEscapeKey(handler, enabled = true) {
|
|
39
|
+
react.useEffect(() => {
|
|
40
|
+
if (reactNative.Platform.OS !== "web" || !enabled) return;
|
|
41
|
+
const listener = (e) => {
|
|
42
|
+
if (e.key === "Escape") handler();
|
|
43
|
+
};
|
|
44
|
+
document.addEventListener("keydown", listener);
|
|
45
|
+
return () => {
|
|
46
|
+
document.removeEventListener("keydown", listener);
|
|
47
|
+
};
|
|
48
|
+
}, [handler, enabled]);
|
|
49
|
+
}
|
|
50
|
+
var HIGH_CONTRAST_QUERY = "(prefers-contrast: more)";
|
|
51
|
+
function useHighContrast() {
|
|
52
|
+
const [isHighContrast, setIsHighContrast] = react.useState(() => {
|
|
53
|
+
if (reactNative.Platform.OS !== "web") return false;
|
|
54
|
+
return window.matchMedia(HIGH_CONTRAST_QUERY).matches;
|
|
55
|
+
});
|
|
56
|
+
react.useEffect(() => {
|
|
57
|
+
if (reactNative.Platform.OS !== "web") return;
|
|
58
|
+
const mql = window.matchMedia(HIGH_CONTRAST_QUERY);
|
|
59
|
+
const handler = (e) => {
|
|
60
|
+
setIsHighContrast(e.matches);
|
|
61
|
+
};
|
|
62
|
+
mql.addEventListener("change", handler);
|
|
63
|
+
return () => {
|
|
64
|
+
mql.removeEventListener("change", handler);
|
|
65
|
+
};
|
|
66
|
+
}, []);
|
|
67
|
+
return isHighContrast;
|
|
68
|
+
}
|
|
69
|
+
function debounce(fn, wait = 300) {
|
|
70
|
+
let timeoutId = null;
|
|
71
|
+
return function debounced(...args) {
|
|
72
|
+
if (utils.isValueDefined(timeoutId))
|
|
73
|
+
clearTimeout(timeoutId);
|
|
74
|
+
timeoutId = setTimeout(() => {
|
|
75
|
+
fn(...args);
|
|
76
|
+
timeoutId = null;
|
|
77
|
+
}, wait);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function throttle(fn, wait = 300) {
|
|
81
|
+
let lastCallTime = null;
|
|
82
|
+
let timeoutId = null;
|
|
83
|
+
return function throttled(...args) {
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
if (!utils.isValueDefined(lastCallTime) || now - lastCallTime >= wait) {
|
|
86
|
+
lastCallTime = now;
|
|
87
|
+
fn(...args);
|
|
88
|
+
} else {
|
|
89
|
+
if (utils.isValueDefined(timeoutId))
|
|
90
|
+
clearTimeout(timeoutId);
|
|
91
|
+
const remaining = wait - (now - lastCallTime);
|
|
92
|
+
timeoutId = setTimeout(() => {
|
|
93
|
+
lastCallTime = Date.now();
|
|
94
|
+
fn(...args);
|
|
95
|
+
timeoutId = null;
|
|
96
|
+
}, remaining);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function flushPending(stateRef) {
|
|
101
|
+
const state = stateRef.current;
|
|
102
|
+
if (!utils.isValueDefined(state.timeoutId) || !utils.isValueDefined(state.pendingArgs)) return;
|
|
103
|
+
clearTimeout(state.timeoutId);
|
|
104
|
+
const args = state.pendingArgs;
|
|
105
|
+
state.timeoutId = null;
|
|
106
|
+
state.pendingArgs = null;
|
|
107
|
+
state.callback(...args);
|
|
108
|
+
}
|
|
109
|
+
function cancelPending(stateRef) {
|
|
110
|
+
const state = stateRef.current;
|
|
111
|
+
if (utils.isValueDefined(state.timeoutId)) clearTimeout(state.timeoutId);
|
|
112
|
+
state.timeoutId = null;
|
|
113
|
+
state.pendingArgs = null;
|
|
114
|
+
}
|
|
115
|
+
function schedulePending(stateRef, args, wait) {
|
|
116
|
+
const state = stateRef.current;
|
|
117
|
+
if (utils.isValueDefined(state.timeoutId)) clearTimeout(state.timeoutId);
|
|
118
|
+
state.pendingArgs = args;
|
|
119
|
+
state.timeoutId = setTimeout(() => {
|
|
120
|
+
state.callback(...args);
|
|
121
|
+
state.timeoutId = null;
|
|
122
|
+
state.pendingArgs = null;
|
|
123
|
+
}, wait);
|
|
124
|
+
}
|
|
125
|
+
function useDebouncedCallback(callback, wait) {
|
|
126
|
+
const stateRef = react.useRef({ timeoutId: null, pendingArgs: null, callback });
|
|
127
|
+
react.useEffect(() => {
|
|
128
|
+
stateRef.current.callback = callback;
|
|
129
|
+
}, [callback]);
|
|
130
|
+
const result = react.useMemo(
|
|
131
|
+
() => Object.assign(
|
|
132
|
+
(...args) => {
|
|
133
|
+
schedulePending(stateRef, args, wait);
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
flush: () => {
|
|
137
|
+
flushPending(stateRef);
|
|
138
|
+
},
|
|
139
|
+
cancel: () => {
|
|
140
|
+
cancelPending(stateRef);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
),
|
|
144
|
+
[wait]
|
|
145
|
+
);
|
|
146
|
+
react.useEffect(() => () => {
|
|
147
|
+
result.flush();
|
|
148
|
+
}, [result]);
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
exports.debounce = debounce;
|
|
153
|
+
exports.throttle = throttle;
|
|
154
|
+
exports.useDebouncedCallback = useDebouncedCallback;
|
|
155
|
+
exports.useEscapeKey = useEscapeKey;
|
|
156
|
+
exports.useHighContrast = useHighContrast;
|
|
157
|
+
exports.useOnlineStatus = useOnlineStatus;
|
|
158
|
+
//# sourceMappingURL=index.js.map
|
|
159
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/useOnlineStatus.ts","../src/useEscapeKey.ts","../src/useHighContrast.ts","../src/debounce.ts"],"names":["Platform","useState","useCallback","useEffect","isValueDefined","useRef","useMemo"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Hook to track the browser's online/offline status.\n * Web-only — native apps use React Query's built-in caching.\n *\n * Uses `navigator.onLine` for initial state and listens to\n * `online`/`offline` window events for real-time updates.\n */\nimport { useCallback, useEffect, useState } from 'react';\n\nimport { Platform } from 'react-native';\n\ninterface OnlineStatus {\n /** Whether the browser currently has network connectivity. */\n isOnline: boolean;\n /** Whether the browser is currently offline. */\n isOffline: boolean;\n}\n\n/** Returns true when running on the web platform. */\nfunction isWebPlatform(): boolean {\n return Platform.OS === 'web';\n}\n\n/** Reads the current browser online state safely. */\nfunction getBrowserOnlineState(): boolean {\n if (typeof navigator === 'undefined') return true;\n return navigator.onLine;\n}\n\n/**\n * Tracks browser online/offline status via `navigator.onLine`\n * and the `online`/`offline` window events.\n *\n * On native platforms, always reports as online (native apps\n * handle connectivity differently via React Query).\n */\nexport function useOnlineStatus(): OnlineStatus {\n const [isOnline, setIsOnline] = useState<boolean>(() => {\n if (!isWebPlatform()) return true;\n return getBrowserOnlineState();\n });\n\n const handleOnline = useCallback((): void => {\n setIsOnline(true);\n }, []);\n\n const handleOffline = useCallback((): void => {\n setIsOnline(false);\n }, []);\n\n useEffect(() => {\n if (!isWebPlatform()) return;\n if (typeof window === 'undefined') return;\n\n window.addEventListener('online', handleOnline);\n window.addEventListener('offline', handleOffline);\n\n return () => {\n window.removeEventListener('online', handleOnline);\n window.removeEventListener('offline', handleOffline);\n };\n }, [handleOnline, handleOffline]);\n\n return { isOnline, isOffline: !isOnline };\n}\n","/**\n * Calls the provided handler when the Escape key is pressed.\n * Web only -- no-op on native platforms.\n */\nimport { useEffect } from 'react';\n\nimport { Platform } from 'react-native';\n\nexport function useEscapeKey(handler: () => void, enabled = true): void {\n useEffect(() => {\n if (Platform.OS !== 'web' || !enabled) return;\n\n const listener = (e: KeyboardEvent): void => {\n if (e.key === 'Escape') handler();\n };\n\n document.addEventListener('keydown', listener);\n return () => {\n document.removeEventListener('keydown', listener);\n };\n }, [handler, enabled]);\n}\n","/**\n * Detects whether the user has enabled a high contrast preference\n * via the `prefers-contrast: more` media query.\n *\n * Web only -- returns `false` on native platforms where the media\n * query is not available.\n */\nimport { useEffect, useState } from 'react';\n\nimport { Platform } from 'react-native';\n\nconst HIGH_CONTRAST_QUERY = '(prefers-contrast: more)';\n\nexport function useHighContrast(): boolean {\n const [isHighContrast, setIsHighContrast] = useState(() => {\n if (Platform.OS !== 'web') return false;\n return window.matchMedia(HIGH_CONTRAST_QUERY).matches;\n });\n\n useEffect(() => {\n if (Platform.OS !== 'web') return;\n\n const mql = window.matchMedia(HIGH_CONTRAST_QUERY);\n const handler = (e: MediaQueryListEvent): void => {\n setIsHighContrast(e.matches);\n };\n\n mql.addEventListener('change', handler);\n return () => {\n mql.removeEventListener('change', handler);\n };\n }, []);\n\n return isHighContrast;\n}\n","/**\n * Debounce and throttle utilities for rate limiting function calls.\n */\n\n/**\n * Creates a debounced version of a function that delays invoking until after\n * `wait` milliseconds have elapsed since the last time it was invoked.\n *\n * @param fn - The function to debounce\n * @param wait - The number of milliseconds to delay (default: 300)\n * @returns A debounced version of the function\n *\n * @example\n * const debouncedSave = debounce(saveData, 500);\n * // Rapid calls will only trigger saveData after 500ms of inactivity\n */\n/**\n * React hook for creating a debounced callback.\n * The callback reference is stable and won't cause re-renders.\n *\n * @param callback - The callback function to debounce\n * @param wait - The debounce delay in milliseconds\n * @param deps - Dependencies array (similar to useCallback)\n * @returns A stable debounced callback\n *\n * @example\n * const debouncedSearch = useDebouncedCallback(\n * (query: string) => searchAPI(query),\n * 300,\n * []\n * );\n */\nimport { useMemo, useRef, useEffect } from 'react';\n\nimport { isValueDefined } from '@dloizides/utils';\n\nexport function debounce<T extends (...args: readonly unknown[]) => unknown>(\n fn: T,\n wait: number = 300\n): (...args: Parameters<T>) => void {\n let timeoutId: ReturnType<typeof setTimeout> | null = null;\n\n return function debounced(...args: Parameters<T>) {\n if (isValueDefined(timeoutId)) \n clearTimeout(timeoutId);\n \n timeoutId = setTimeout(() => {\n fn(...args);\n timeoutId = null;\n }, wait);\n };\n}\n\n/**\n * Creates a throttled version of a function that only invokes at most once\n * per every `wait` milliseconds.\n *\n * @param fn - The function to throttle\n * @param wait - The number of milliseconds to wait between invocations (default: 300)\n * @returns A throttled version of the function\n *\n * @example\n * const throttledUpdate = throttle(updatePosition, 100);\n * // Rapid calls will only trigger updatePosition every 100ms\n */\nexport function throttle<T extends (...args: readonly unknown[]) => unknown>(\n fn: T,\n wait: number = 300\n): (...args: Parameters<T>) => void {\n let lastCallTime: number | null = null;\n let timeoutId: ReturnType<typeof setTimeout> | null = null;\n\n return function throttled(...args: Parameters<T>) {\n const now = Date.now();\n\n if (!isValueDefined(lastCallTime) || now - lastCallTime >= wait) {\n // Enough time has passed, call immediately\n lastCallTime = now;\n fn(...args);\n } else {\n // Schedule a call for the end of the wait period\n if (isValueDefined(timeoutId)) \n clearTimeout(timeoutId);\n \n const remaining = wait - (now - lastCallTime);\n timeoutId = setTimeout(() => {\n lastCallTime = Date.now();\n fn(...args);\n timeoutId = null;\n }, remaining);\n }\n };\n}\n\ninterface DebouncedCallback<T extends (...args: readonly unknown[]) => unknown> {\n (...args: Parameters<T>): void;\n /** Fire the pending call now (if any) and clear it. No-op if nothing pending. */\n flush: () => void;\n /** Drop the pending call (if any) without firing it. No-op if nothing pending. */\n cancel: () => void;\n}\n\n/**\n * Mutable bookkeeping shared by the scheduled call, `flush` and `cancel`. Held\n * in a single ref so the helpers below can be plain module functions (keeps\n * `useDebouncedCallback` itself small and avoids reading refs during render).\n */\ninterface DebounceState<T extends (...args: readonly unknown[]) => unknown> {\n timeoutId: ReturnType<typeof setTimeout> | null;\n pendingArgs: Parameters<T> | null;\n callback: T;\n}\n\ninterface DebounceStateRef<T extends (...args: readonly unknown[]) => unknown> {\n current: DebounceState<T>;\n}\n\n/** Fire the pending call now (if any) and clear it. */\nfunction flushPending<T extends (...args: readonly unknown[]) => unknown>(\n stateRef: DebounceStateRef<T>\n): void {\n const state = stateRef.current;\n if (!isValueDefined(state.timeoutId) || !isValueDefined(state.pendingArgs)) return;\n clearTimeout(state.timeoutId);\n const args = state.pendingArgs;\n state.timeoutId = null;\n state.pendingArgs = null;\n state.callback(...args);\n}\n\n/** Drop the pending call (if any) without firing it. */\nfunction cancelPending<T extends (...args: readonly unknown[]) => unknown>(\n stateRef: DebounceStateRef<T>\n): void {\n const state = stateRef.current;\n if (isValueDefined(state.timeoutId)) clearTimeout(state.timeoutId);\n state.timeoutId = null;\n state.pendingArgs = null;\n}\n\n/** (Re)schedule the trailing-edge call for `wait` ms from now. */\nfunction schedulePending<T extends (...args: readonly unknown[]) => unknown>(\n stateRef: DebounceStateRef<T>,\n args: Parameters<T>,\n wait: number\n): void {\n const state = stateRef.current;\n if (isValueDefined(state.timeoutId)) clearTimeout(state.timeoutId);\n state.pendingArgs = args;\n state.timeoutId = setTimeout(() => {\n state.callback(...args);\n state.timeoutId = null;\n state.pendingArgs = null;\n }, wait);\n}\n\nexport function useDebouncedCallback<T extends (...args: readonly unknown[]) => unknown>(\n callback: T,\n wait: number\n): DebouncedCallback<T> {\n const stateRef = useRef<DebounceState<T>>({ timeoutId: null, pendingArgs: null, callback });\n\n // Update callback ref when callback changes\n useEffect(() => {\n stateRef.current.callback = callback;\n }, [callback]);\n\n // The callable + flush/cancel are built in one expression so no value React\n // has already memoised gets mutated afterwards (react-compiler safe), and no\n // type assertion is needed — Object.assign yields the intersection type.\n const result = useMemo<DebouncedCallback<T>>(\n () =>\n Object.assign(\n (...args: Parameters<T>): void => { schedulePending(stateRef, args, wait); },\n {\n flush: (): void => { flushPending(stateRef); },\n cancel: (): void => { cancelPending(stateRef); },\n }\n ),\n [wait]\n );\n\n // On unmount, fire any pending save synchronously so navigation away\n // doesn't drop in-flight changes (e.g., an uploaded image whose autoSave\n // debounce hadn't fired yet).\n useEffect(() => () => { result.flush(); }, [result]);\n\n return result;\n}\n"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect, useRef, useMemo } from 'react';
|
|
2
|
+
import { Platform } from 'react-native';
|
|
3
|
+
import { isValueDefined } from '@dloizides/utils';
|
|
4
|
+
|
|
5
|
+
// src/useOnlineStatus.ts
|
|
6
|
+
function isWebPlatform() {
|
|
7
|
+
return Platform.OS === "web";
|
|
8
|
+
}
|
|
9
|
+
function getBrowserOnlineState() {
|
|
10
|
+
if (typeof navigator === "undefined") return true;
|
|
11
|
+
return navigator.onLine;
|
|
12
|
+
}
|
|
13
|
+
function useOnlineStatus() {
|
|
14
|
+
const [isOnline, setIsOnline] = useState(() => {
|
|
15
|
+
if (!isWebPlatform()) return true;
|
|
16
|
+
return getBrowserOnlineState();
|
|
17
|
+
});
|
|
18
|
+
const handleOnline = useCallback(() => {
|
|
19
|
+
setIsOnline(true);
|
|
20
|
+
}, []);
|
|
21
|
+
const handleOffline = useCallback(() => {
|
|
22
|
+
setIsOnline(false);
|
|
23
|
+
}, []);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!isWebPlatform()) return;
|
|
26
|
+
if (typeof window === "undefined") return;
|
|
27
|
+
window.addEventListener("online", handleOnline);
|
|
28
|
+
window.addEventListener("offline", handleOffline);
|
|
29
|
+
return () => {
|
|
30
|
+
window.removeEventListener("online", handleOnline);
|
|
31
|
+
window.removeEventListener("offline", handleOffline);
|
|
32
|
+
};
|
|
33
|
+
}, [handleOnline, handleOffline]);
|
|
34
|
+
return { isOnline, isOffline: !isOnline };
|
|
35
|
+
}
|
|
36
|
+
function useEscapeKey(handler, enabled = true) {
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (Platform.OS !== "web" || !enabled) return;
|
|
39
|
+
const listener = (e) => {
|
|
40
|
+
if (e.key === "Escape") handler();
|
|
41
|
+
};
|
|
42
|
+
document.addEventListener("keydown", listener);
|
|
43
|
+
return () => {
|
|
44
|
+
document.removeEventListener("keydown", listener);
|
|
45
|
+
};
|
|
46
|
+
}, [handler, enabled]);
|
|
47
|
+
}
|
|
48
|
+
var HIGH_CONTRAST_QUERY = "(prefers-contrast: more)";
|
|
49
|
+
function useHighContrast() {
|
|
50
|
+
const [isHighContrast, setIsHighContrast] = useState(() => {
|
|
51
|
+
if (Platform.OS !== "web") return false;
|
|
52
|
+
return window.matchMedia(HIGH_CONTRAST_QUERY).matches;
|
|
53
|
+
});
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (Platform.OS !== "web") return;
|
|
56
|
+
const mql = window.matchMedia(HIGH_CONTRAST_QUERY);
|
|
57
|
+
const handler = (e) => {
|
|
58
|
+
setIsHighContrast(e.matches);
|
|
59
|
+
};
|
|
60
|
+
mql.addEventListener("change", handler);
|
|
61
|
+
return () => {
|
|
62
|
+
mql.removeEventListener("change", handler);
|
|
63
|
+
};
|
|
64
|
+
}, []);
|
|
65
|
+
return isHighContrast;
|
|
66
|
+
}
|
|
67
|
+
function debounce(fn, wait = 300) {
|
|
68
|
+
let timeoutId = null;
|
|
69
|
+
return function debounced(...args) {
|
|
70
|
+
if (isValueDefined(timeoutId))
|
|
71
|
+
clearTimeout(timeoutId);
|
|
72
|
+
timeoutId = setTimeout(() => {
|
|
73
|
+
fn(...args);
|
|
74
|
+
timeoutId = null;
|
|
75
|
+
}, wait);
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function throttle(fn, wait = 300) {
|
|
79
|
+
let lastCallTime = null;
|
|
80
|
+
let timeoutId = null;
|
|
81
|
+
return function throttled(...args) {
|
|
82
|
+
const now = Date.now();
|
|
83
|
+
if (!isValueDefined(lastCallTime) || now - lastCallTime >= wait) {
|
|
84
|
+
lastCallTime = now;
|
|
85
|
+
fn(...args);
|
|
86
|
+
} else {
|
|
87
|
+
if (isValueDefined(timeoutId))
|
|
88
|
+
clearTimeout(timeoutId);
|
|
89
|
+
const remaining = wait - (now - lastCallTime);
|
|
90
|
+
timeoutId = setTimeout(() => {
|
|
91
|
+
lastCallTime = Date.now();
|
|
92
|
+
fn(...args);
|
|
93
|
+
timeoutId = null;
|
|
94
|
+
}, remaining);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function flushPending(stateRef) {
|
|
99
|
+
const state = stateRef.current;
|
|
100
|
+
if (!isValueDefined(state.timeoutId) || !isValueDefined(state.pendingArgs)) return;
|
|
101
|
+
clearTimeout(state.timeoutId);
|
|
102
|
+
const args = state.pendingArgs;
|
|
103
|
+
state.timeoutId = null;
|
|
104
|
+
state.pendingArgs = null;
|
|
105
|
+
state.callback(...args);
|
|
106
|
+
}
|
|
107
|
+
function cancelPending(stateRef) {
|
|
108
|
+
const state = stateRef.current;
|
|
109
|
+
if (isValueDefined(state.timeoutId)) clearTimeout(state.timeoutId);
|
|
110
|
+
state.timeoutId = null;
|
|
111
|
+
state.pendingArgs = null;
|
|
112
|
+
}
|
|
113
|
+
function schedulePending(stateRef, args, wait) {
|
|
114
|
+
const state = stateRef.current;
|
|
115
|
+
if (isValueDefined(state.timeoutId)) clearTimeout(state.timeoutId);
|
|
116
|
+
state.pendingArgs = args;
|
|
117
|
+
state.timeoutId = setTimeout(() => {
|
|
118
|
+
state.callback(...args);
|
|
119
|
+
state.timeoutId = null;
|
|
120
|
+
state.pendingArgs = null;
|
|
121
|
+
}, wait);
|
|
122
|
+
}
|
|
123
|
+
function useDebouncedCallback(callback, wait) {
|
|
124
|
+
const stateRef = useRef({ timeoutId: null, pendingArgs: null, callback });
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
stateRef.current.callback = callback;
|
|
127
|
+
}, [callback]);
|
|
128
|
+
const result = useMemo(
|
|
129
|
+
() => Object.assign(
|
|
130
|
+
(...args) => {
|
|
131
|
+
schedulePending(stateRef, args, wait);
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
flush: () => {
|
|
135
|
+
flushPending(stateRef);
|
|
136
|
+
},
|
|
137
|
+
cancel: () => {
|
|
138
|
+
cancelPending(stateRef);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
),
|
|
142
|
+
[wait]
|
|
143
|
+
);
|
|
144
|
+
useEffect(() => () => {
|
|
145
|
+
result.flush();
|
|
146
|
+
}, [result]);
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export { debounce, throttle, useDebouncedCallback, useEscapeKey, useHighContrast, useOnlineStatus };
|
|
151
|
+
//# sourceMappingURL=index.mjs.map
|
|
152
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/useOnlineStatus.ts","../src/useEscapeKey.ts","../src/useHighContrast.ts","../src/debounce.ts"],"names":["useEffect","Platform","useState"],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * Hook to track the browser's online/offline status.\n * Web-only — native apps use React Query's built-in caching.\n *\n * Uses `navigator.onLine` for initial state and listens to\n * `online`/`offline` window events for real-time updates.\n */\nimport { useCallback, useEffect, useState } from 'react';\n\nimport { Platform } from 'react-native';\n\ninterface OnlineStatus {\n /** Whether the browser currently has network connectivity. */\n isOnline: boolean;\n /** Whether the browser is currently offline. */\n isOffline: boolean;\n}\n\n/** Returns true when running on the web platform. */\nfunction isWebPlatform(): boolean {\n return Platform.OS === 'web';\n}\n\n/** Reads the current browser online state safely. */\nfunction getBrowserOnlineState(): boolean {\n if (typeof navigator === 'undefined') return true;\n return navigator.onLine;\n}\n\n/**\n * Tracks browser online/offline status via `navigator.onLine`\n * and the `online`/`offline` window events.\n *\n * On native platforms, always reports as online (native apps\n * handle connectivity differently via React Query).\n */\nexport function useOnlineStatus(): OnlineStatus {\n const [isOnline, setIsOnline] = useState<boolean>(() => {\n if (!isWebPlatform()) return true;\n return getBrowserOnlineState();\n });\n\n const handleOnline = useCallback((): void => {\n setIsOnline(true);\n }, []);\n\n const handleOffline = useCallback((): void => {\n setIsOnline(false);\n }, []);\n\n useEffect(() => {\n if (!isWebPlatform()) return;\n if (typeof window === 'undefined') return;\n\n window.addEventListener('online', handleOnline);\n window.addEventListener('offline', handleOffline);\n\n return () => {\n window.removeEventListener('online', handleOnline);\n window.removeEventListener('offline', handleOffline);\n };\n }, [handleOnline, handleOffline]);\n\n return { isOnline, isOffline: !isOnline };\n}\n","/**\n * Calls the provided handler when the Escape key is pressed.\n * Web only -- no-op on native platforms.\n */\nimport { useEffect } from 'react';\n\nimport { Platform } from 'react-native';\n\nexport function useEscapeKey(handler: () => void, enabled = true): void {\n useEffect(() => {\n if (Platform.OS !== 'web' || !enabled) return;\n\n const listener = (e: KeyboardEvent): void => {\n if (e.key === 'Escape') handler();\n };\n\n document.addEventListener('keydown', listener);\n return () => {\n document.removeEventListener('keydown', listener);\n };\n }, [handler, enabled]);\n}\n","/**\n * Detects whether the user has enabled a high contrast preference\n * via the `prefers-contrast: more` media query.\n *\n * Web only -- returns `false` on native platforms where the media\n * query is not available.\n */\nimport { useEffect, useState } from 'react';\n\nimport { Platform } from 'react-native';\n\nconst HIGH_CONTRAST_QUERY = '(prefers-contrast: more)';\n\nexport function useHighContrast(): boolean {\n const [isHighContrast, setIsHighContrast] = useState(() => {\n if (Platform.OS !== 'web') return false;\n return window.matchMedia(HIGH_CONTRAST_QUERY).matches;\n });\n\n useEffect(() => {\n if (Platform.OS !== 'web') return;\n\n const mql = window.matchMedia(HIGH_CONTRAST_QUERY);\n const handler = (e: MediaQueryListEvent): void => {\n setIsHighContrast(e.matches);\n };\n\n mql.addEventListener('change', handler);\n return () => {\n mql.removeEventListener('change', handler);\n };\n }, []);\n\n return isHighContrast;\n}\n","/**\n * Debounce and throttle utilities for rate limiting function calls.\n */\n\n/**\n * Creates a debounced version of a function that delays invoking until after\n * `wait` milliseconds have elapsed since the last time it was invoked.\n *\n * @param fn - The function to debounce\n * @param wait - The number of milliseconds to delay (default: 300)\n * @returns A debounced version of the function\n *\n * @example\n * const debouncedSave = debounce(saveData, 500);\n * // Rapid calls will only trigger saveData after 500ms of inactivity\n */\n/**\n * React hook for creating a debounced callback.\n * The callback reference is stable and won't cause re-renders.\n *\n * @param callback - The callback function to debounce\n * @param wait - The debounce delay in milliseconds\n * @param deps - Dependencies array (similar to useCallback)\n * @returns A stable debounced callback\n *\n * @example\n * const debouncedSearch = useDebouncedCallback(\n * (query: string) => searchAPI(query),\n * 300,\n * []\n * );\n */\nimport { useMemo, useRef, useEffect } from 'react';\n\nimport { isValueDefined } from '@dloizides/utils';\n\nexport function debounce<T extends (...args: readonly unknown[]) => unknown>(\n fn: T,\n wait: number = 300\n): (...args: Parameters<T>) => void {\n let timeoutId: ReturnType<typeof setTimeout> | null = null;\n\n return function debounced(...args: Parameters<T>) {\n if (isValueDefined(timeoutId)) \n clearTimeout(timeoutId);\n \n timeoutId = setTimeout(() => {\n fn(...args);\n timeoutId = null;\n }, wait);\n };\n}\n\n/**\n * Creates a throttled version of a function that only invokes at most once\n * per every `wait` milliseconds.\n *\n * @param fn - The function to throttle\n * @param wait - The number of milliseconds to wait between invocations (default: 300)\n * @returns A throttled version of the function\n *\n * @example\n * const throttledUpdate = throttle(updatePosition, 100);\n * // Rapid calls will only trigger updatePosition every 100ms\n */\nexport function throttle<T extends (...args: readonly unknown[]) => unknown>(\n fn: T,\n wait: number = 300\n): (...args: Parameters<T>) => void {\n let lastCallTime: number | null = null;\n let timeoutId: ReturnType<typeof setTimeout> | null = null;\n\n return function throttled(...args: Parameters<T>) {\n const now = Date.now();\n\n if (!isValueDefined(lastCallTime) || now - lastCallTime >= wait) {\n // Enough time has passed, call immediately\n lastCallTime = now;\n fn(...args);\n } else {\n // Schedule a call for the end of the wait period\n if (isValueDefined(timeoutId)) \n clearTimeout(timeoutId);\n \n const remaining = wait - (now - lastCallTime);\n timeoutId = setTimeout(() => {\n lastCallTime = Date.now();\n fn(...args);\n timeoutId = null;\n }, remaining);\n }\n };\n}\n\ninterface DebouncedCallback<T extends (...args: readonly unknown[]) => unknown> {\n (...args: Parameters<T>): void;\n /** Fire the pending call now (if any) and clear it. No-op if nothing pending. */\n flush: () => void;\n /** Drop the pending call (if any) without firing it. No-op if nothing pending. */\n cancel: () => void;\n}\n\n/**\n * Mutable bookkeeping shared by the scheduled call, `flush` and `cancel`. Held\n * in a single ref so the helpers below can be plain module functions (keeps\n * `useDebouncedCallback` itself small and avoids reading refs during render).\n */\ninterface DebounceState<T extends (...args: readonly unknown[]) => unknown> {\n timeoutId: ReturnType<typeof setTimeout> | null;\n pendingArgs: Parameters<T> | null;\n callback: T;\n}\n\ninterface DebounceStateRef<T extends (...args: readonly unknown[]) => unknown> {\n current: DebounceState<T>;\n}\n\n/** Fire the pending call now (if any) and clear it. */\nfunction flushPending<T extends (...args: readonly unknown[]) => unknown>(\n stateRef: DebounceStateRef<T>\n): void {\n const state = stateRef.current;\n if (!isValueDefined(state.timeoutId) || !isValueDefined(state.pendingArgs)) return;\n clearTimeout(state.timeoutId);\n const args = state.pendingArgs;\n state.timeoutId = null;\n state.pendingArgs = null;\n state.callback(...args);\n}\n\n/** Drop the pending call (if any) without firing it. */\nfunction cancelPending<T extends (...args: readonly unknown[]) => unknown>(\n stateRef: DebounceStateRef<T>\n): void {\n const state = stateRef.current;\n if (isValueDefined(state.timeoutId)) clearTimeout(state.timeoutId);\n state.timeoutId = null;\n state.pendingArgs = null;\n}\n\n/** (Re)schedule the trailing-edge call for `wait` ms from now. */\nfunction schedulePending<T extends (...args: readonly unknown[]) => unknown>(\n stateRef: DebounceStateRef<T>,\n args: Parameters<T>,\n wait: number\n): void {\n const state = stateRef.current;\n if (isValueDefined(state.timeoutId)) clearTimeout(state.timeoutId);\n state.pendingArgs = args;\n state.timeoutId = setTimeout(() => {\n state.callback(...args);\n state.timeoutId = null;\n state.pendingArgs = null;\n }, wait);\n}\n\nexport function useDebouncedCallback<T extends (...args: readonly unknown[]) => unknown>(\n callback: T,\n wait: number\n): DebouncedCallback<T> {\n const stateRef = useRef<DebounceState<T>>({ timeoutId: null, pendingArgs: null, callback });\n\n // Update callback ref when callback changes\n useEffect(() => {\n stateRef.current.callback = callback;\n }, [callback]);\n\n // The callable + flush/cancel are built in one expression so no value React\n // has already memoised gets mutated afterwards (react-compiler safe), and no\n // type assertion is needed — Object.assign yields the intersection type.\n const result = useMemo<DebouncedCallback<T>>(\n () =>\n Object.assign(\n (...args: Parameters<T>): void => { schedulePending(stateRef, args, wait); },\n {\n flush: (): void => { flushPending(stateRef); },\n cancel: (): void => { cancelPending(stateRef); },\n }\n ),\n [wait]\n );\n\n // On unmount, fire any pending save synchronously so navigation away\n // doesn't drop in-flight changes (e.g., an uploaded image whose autoSave\n // debounce hadn't fired yet).\n useEffect(() => () => { result.flush(); }, [result]);\n\n return result;\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dloizides/rn-web-hooks",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Cross-platform (React Native + web) React hooks and rate-limiting helpers: useOnlineStatus, useEscapeKey, useHighContrast, debounce/throttle/useDebouncedCallback.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react-hooks",
|
|
7
|
+
"react-native",
|
|
8
|
+
"react-native-web",
|
|
9
|
+
"debounce",
|
|
10
|
+
"throttle",
|
|
11
|
+
"online-status"
|
|
12
|
+
],
|
|
13
|
+
"author": "dloizides",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/openmindednewby/rn-web-hooks.git"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/openmindednewby/rn-web-hooks#readme",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/openmindednewby/rn-web-hooks/issues"
|
|
22
|
+
},
|
|
23
|
+
"main": "./dist/index.js",
|
|
24
|
+
"module": "./dist/index.mjs",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"require": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"default": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"import": {
|
|
33
|
+
"types": "./dist/index.d.mts",
|
|
34
|
+
"default": "./dist/index.mjs"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"README.md",
|
|
41
|
+
"CHANGELOG.md"
|
|
42
|
+
],
|
|
43
|
+
"sideEffects": false,
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=18.0.0"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "rimraf dist && tsup",
|
|
49
|
+
"build:watch": "tsup --watch",
|
|
50
|
+
"lint": "eslint src --ext .ts",
|
|
51
|
+
"typecheck": "tsc --noEmit",
|
|
52
|
+
"clean": "rimraf dist",
|
|
53
|
+
"security:audit": "npm audit --audit-level=high",
|
|
54
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@dloizides/utils": "^1.1.0"
|
|
58
|
+
},
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"react": ">=18.0.0",
|
|
61
|
+
"react-native": ">=0.79.0"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@types/node": "^20.19.32",
|
|
65
|
+
"@types/react": "^18.2.0",
|
|
66
|
+
"react": "^18.2.0",
|
|
67
|
+
"react-native": "0.81.5",
|
|
68
|
+
"rimraf": "^5.0.0",
|
|
69
|
+
"tsup": "^8.0.0",
|
|
70
|
+
"typescript": "^5.4.0"
|
|
71
|
+
}
|
|
72
|
+
}
|