airyhooks 0.2.0 → 0.3.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 +54 -8
- package/dist/commands/add.js +46 -9
- package/dist/commands/add.js.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/utils/config.js +2 -0
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/get-file-extension.js +3 -0
- package/dist/utils/get-file-extension.js.map +1 -1
- package/dist/utils/get-hook-template.js +9 -1426
- package/dist/utils/get-hook-template.js.map +1 -1
- package/dist/utils/hook-templates.js +3386 -0
- package/dist/utils/hook-templates.js.map +1 -0
- package/dist/utils/registry.js +8 -0
- package/dist/utils/registry.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,1431 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
// Generated by: pnpm --filter @airyhooks/hooks build:templates
|
|
3
|
-
// Source: packages/hooks/src/*/use*.ts
|
|
4
|
-
const templates = {
|
|
5
|
-
useBoolean: `import { useCallback, useState } from "react";
|
|
6
|
-
|
|
1
|
+
import { templates } from "./hook-templates.js";
|
|
7
2
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* @param initialValue - Initial boolean value (default: false)
|
|
11
|
-
* @returns Tuple of [value, { setTrue, setFalse, toggle }]
|
|
12
|
-
*
|
|
13
|
-
* @example
|
|
14
|
-
* const [isEnabled, handlers] = useBoolean(false);
|
|
15
|
-
*
|
|
16
|
-
* return (
|
|
17
|
-
* <>
|
|
18
|
-
* <button onClick={handlers.toggle}>Toggle</button>
|
|
19
|
-
* <button onClick={handlers.setTrue}>Enable</button>
|
|
20
|
-
* <button onClick={handlers.setFalse}>Disable</button>
|
|
21
|
-
* </>
|
|
22
|
-
* );
|
|
3
|
+
* Get the hook template from the generated template file.
|
|
23
4
|
*/
|
|
24
|
-
export function useBoolean(initialValue = false): [
|
|
25
|
-
boolean,
|
|
26
|
-
{
|
|
27
|
-
setFalse: () => void;
|
|
28
|
-
setTrue: () => void;
|
|
29
|
-
toggle: () => void;
|
|
30
|
-
},
|
|
31
|
-
] {
|
|
32
|
-
const [value, setValue] = useState(initialValue);
|
|
33
|
-
|
|
34
|
-
const toggle = useCallback(() => {
|
|
35
|
-
setValue((prev) => !prev);
|
|
36
|
-
}, []);
|
|
37
|
-
|
|
38
|
-
const setTrue = useCallback(() => {
|
|
39
|
-
setValue(true);
|
|
40
|
-
}, []);
|
|
41
|
-
|
|
42
|
-
const setFalse = useCallback(() => {
|
|
43
|
-
setValue(false);
|
|
44
|
-
}, []);
|
|
45
|
-
|
|
46
|
-
return [
|
|
47
|
-
value,
|
|
48
|
-
{
|
|
49
|
-
setFalse,
|
|
50
|
-
setTrue,
|
|
51
|
-
toggle,
|
|
52
|
-
},
|
|
53
|
-
];
|
|
54
|
-
}
|
|
55
|
-
`,
|
|
56
|
-
useClickAway: `import { useEffect } from "react";
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Detects clicks outside of a target element.
|
|
60
|
-
*
|
|
61
|
-
* @param ref - React ref to the target element
|
|
62
|
-
* @param callback - Function to call when click outside is detected
|
|
63
|
-
*
|
|
64
|
-
* @example
|
|
65
|
-
* const ref = useRef<HTMLDivElement>(null);
|
|
66
|
-
*
|
|
67
|
-
* useClickAway(ref, () => {
|
|
68
|
-
* setIsOpen(false);
|
|
69
|
-
* });
|
|
70
|
-
*
|
|
71
|
-
* return <div ref={ref}>Content</div>;
|
|
72
|
-
*/
|
|
73
|
-
export function useClickAway<T extends HTMLElement>(
|
|
74
|
-
ref: React.RefObject<null | T>,
|
|
75
|
-
callback: () => void,
|
|
76
|
-
): void {
|
|
77
|
-
useEffect(() => {
|
|
78
|
-
const handleClickOutside = (event: MouseEvent) => {
|
|
79
|
-
const element = ref.current;
|
|
80
|
-
if (element && !element.contains(event.target as Node)) {
|
|
81
|
-
callback();
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
document.addEventListener("mousedown", handleClickOutside);
|
|
86
|
-
return () => {
|
|
87
|
-
document.removeEventListener("mousedown", handleClickOutside);
|
|
88
|
-
};
|
|
89
|
-
}, [ref, callback]);
|
|
90
|
-
}
|
|
91
|
-
`,
|
|
92
|
-
useCopyToClipboard: `import { useCallback, useState } from "react";
|
|
93
|
-
|
|
94
|
-
export interface UseCopyToClipboardResult {
|
|
95
|
-
/** The currently copied text, or null if nothing has been copied */
|
|
96
|
-
copiedText: null | string;
|
|
97
|
-
/** Function to copy text to clipboard. Returns true if successful. */
|
|
98
|
-
copy: (text: string) => Promise<boolean>;
|
|
99
|
-
/** Function to reset the copied state */
|
|
100
|
-
reset: () => void;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Copy text to the clipboard using the modern Clipboard API.
|
|
105
|
-
*
|
|
106
|
-
* @returns Object containing copiedText state, copy function, and reset function
|
|
107
|
-
*
|
|
108
|
-
* @example
|
|
109
|
-
* const { copiedText, copy, reset } = useCopyToClipboard();
|
|
110
|
-
*
|
|
111
|
-
* return (
|
|
112
|
-
* <button onClick={() => copy("Hello, World!")}>
|
|
113
|
-
* {copiedText ? "Copied!" : "Copy"}
|
|
114
|
-
* </button>
|
|
115
|
-
* );
|
|
116
|
-
*/
|
|
117
|
-
export function useCopyToClipboard(): UseCopyToClipboardResult {
|
|
118
|
-
const [copiedText, setCopiedText] = useState<null | string>(null);
|
|
119
|
-
|
|
120
|
-
const copy = useCallback(async (text: string): Promise<boolean> => {
|
|
121
|
-
// Check if we're in a browser environment with clipboard support
|
|
122
|
-
const clipboard =
|
|
123
|
-
typeof window !== "undefined" ? navigator.clipboard : undefined;
|
|
124
|
-
|
|
125
|
-
if (!clipboard) {
|
|
126
|
-
console.warn("Clipboard API not available");
|
|
127
|
-
return false;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
try {
|
|
131
|
-
await clipboard.writeText(text);
|
|
132
|
-
setCopiedText(text);
|
|
133
|
-
return true;
|
|
134
|
-
} catch (error) {
|
|
135
|
-
console.warn("Failed to copy to clipboard:", error);
|
|
136
|
-
setCopiedText(null);
|
|
137
|
-
return false;
|
|
138
|
-
}
|
|
139
|
-
}, []);
|
|
140
|
-
|
|
141
|
-
const reset = useCallback(() => {
|
|
142
|
-
setCopiedText(null);
|
|
143
|
-
}, []);
|
|
144
|
-
|
|
145
|
-
return { copiedText, copy, reset };
|
|
146
|
-
}
|
|
147
|
-
`,
|
|
148
|
-
useCounter: `import { useCallback, useState } from "react";
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Manages numeric state with increment, decrement, reset, and set methods.
|
|
152
|
-
*
|
|
153
|
-
* @param initialValue - Initial numeric value (default: 0)
|
|
154
|
-
* @returns Tuple of [value, { increment, decrement, reset, set }]
|
|
155
|
-
*
|
|
156
|
-
* @example
|
|
157
|
-
* const [count, { increment, decrement, reset }] = useCounter(0);
|
|
158
|
-
*
|
|
159
|
-
* return (
|
|
160
|
-
* <>
|
|
161
|
-
* <p>Count: {count}</p>
|
|
162
|
-
* <button onClick={() => increment()}>+1</button>
|
|
163
|
-
* <button onClick={() => decrement()}>-1</button>
|
|
164
|
-
* <button onClick={() => increment(5)}>+5</button>
|
|
165
|
-
* <button onClick={() => reset()}>Reset</button>
|
|
166
|
-
* </>
|
|
167
|
-
* );
|
|
168
|
-
*/
|
|
169
|
-
export function useCounter(initialValue = 0): [
|
|
170
|
-
number,
|
|
171
|
-
{
|
|
172
|
-
decrement: (amount?: number) => void;
|
|
173
|
-
increment: (amount?: number) => void;
|
|
174
|
-
reset: () => void;
|
|
175
|
-
set: (value: ((prev: number) => number) | number) => void;
|
|
176
|
-
},
|
|
177
|
-
] {
|
|
178
|
-
const [count, setCount] = useState<number>(initialValue);
|
|
179
|
-
|
|
180
|
-
const increment = useCallback((amount = 1) => {
|
|
181
|
-
setCount((prev) => prev + amount);
|
|
182
|
-
}, []);
|
|
183
|
-
|
|
184
|
-
const decrement = useCallback((amount = 1) => {
|
|
185
|
-
setCount((prev) => prev - amount);
|
|
186
|
-
}, []);
|
|
187
|
-
|
|
188
|
-
const reset = useCallback(() => {
|
|
189
|
-
setCount(initialValue);
|
|
190
|
-
}, [initialValue]);
|
|
191
|
-
|
|
192
|
-
const set = useCallback((value: ((prev: number) => number) | number) => {
|
|
193
|
-
setCount(value);
|
|
194
|
-
}, []);
|
|
195
|
-
|
|
196
|
-
return [
|
|
197
|
-
count,
|
|
198
|
-
{
|
|
199
|
-
decrement,
|
|
200
|
-
increment,
|
|
201
|
-
reset,
|
|
202
|
-
set,
|
|
203
|
-
},
|
|
204
|
-
];
|
|
205
|
-
}
|
|
206
|
-
`,
|
|
207
|
-
useDebounce: `import { useEffect, useState } from "react";
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Debounces a value by delaying updates until after the specified delay.
|
|
211
|
-
*
|
|
212
|
-
* @param value - The value to debounce
|
|
213
|
-
* @param delay - The delay in milliseconds (default: 500ms)
|
|
214
|
-
* @returns The debounced value
|
|
215
|
-
*
|
|
216
|
-
* @example
|
|
217
|
-
* const [search, setSearch] = useState("");
|
|
218
|
-
* const debouncedSearch = useDebounce(search, 300);
|
|
219
|
-
*
|
|
220
|
-
* useEffect(() => {
|
|
221
|
-
* // This effect runs 300ms after the user stops typing
|
|
222
|
-
* fetchResults(debouncedSearch);
|
|
223
|
-
* }, [debouncedSearch]);
|
|
224
|
-
*/
|
|
225
|
-
export function useDebounce<T>(value: T, delay = 500): T {
|
|
226
|
-
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
|
227
|
-
|
|
228
|
-
useEffect(() => {
|
|
229
|
-
const timer = setTimeout(() => {
|
|
230
|
-
setDebouncedValue(value);
|
|
231
|
-
}, delay);
|
|
232
|
-
|
|
233
|
-
return () => {
|
|
234
|
-
clearTimeout(timer);
|
|
235
|
-
};
|
|
236
|
-
}, [value, delay]);
|
|
237
|
-
|
|
238
|
-
return debouncedValue;
|
|
239
|
-
}
|
|
240
|
-
`,
|
|
241
|
-
useDocumentTitle: `import { useEffect, useRef } from "react";
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Dynamically update the document title.
|
|
245
|
-
*
|
|
246
|
-
* @param title - The title to set for the document
|
|
247
|
-
* @param restoreOnUnmount - Whether to restore the previous title on unmount (default: true)
|
|
248
|
-
*
|
|
249
|
-
* @example
|
|
250
|
-
* // Basic usage
|
|
251
|
-
* useDocumentTitle('Home | My App');
|
|
252
|
-
*
|
|
253
|
-
* @example
|
|
254
|
-
* // Dynamic title based on state
|
|
255
|
-
* useDocumentTitle(\`\${unreadCount} new messages\`);
|
|
256
|
-
*
|
|
257
|
-
* @example
|
|
258
|
-
* // Don't restore title on unmount
|
|
259
|
-
* useDocumentTitle('Dashboard', false);
|
|
260
|
-
*/
|
|
261
|
-
export function useDocumentTitle(title: string, restoreOnUnmount = true): void {
|
|
262
|
-
const previousTitle = useRef<string | undefined>(undefined);
|
|
263
|
-
|
|
264
|
-
useEffect(() => {
|
|
265
|
-
if (typeof document === "undefined") {
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Store the previous title only on first mount
|
|
270
|
-
previousTitle.current ??= document.title;
|
|
271
|
-
|
|
272
|
-
document.title = title;
|
|
273
|
-
}, [title]);
|
|
274
|
-
|
|
275
|
-
useEffect(() => {
|
|
276
|
-
return () => {
|
|
277
|
-
if (restoreOnUnmount && previousTitle.current !== undefined) {
|
|
278
|
-
document.title = previousTitle.current;
|
|
279
|
-
}
|
|
280
|
-
};
|
|
281
|
-
}, [restoreOnUnmount]);
|
|
282
|
-
}
|
|
283
|
-
`,
|
|
284
|
-
useEventListener: `import { useEffect, useRef } from "react";
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Attaches an event listener to a target element or window with automatic cleanup.
|
|
288
|
-
*
|
|
289
|
-
* @param eventName - The event type to listen for (e.g., 'click', 'scroll', 'keydown')
|
|
290
|
-
* @param handler - The event handler function
|
|
291
|
-
* @param element - The target element or window (default: window)
|
|
292
|
-
* @param options - Event listener options (capture, passive, once)
|
|
293
|
-
*
|
|
294
|
-
* @example
|
|
295
|
-
* // Listen for clicks on window
|
|
296
|
-
* useEventListener('click', (e) => console.log('Clicked!'));
|
|
297
|
-
*
|
|
298
|
-
* @example
|
|
299
|
-
* // Listen for clicks on a specific element
|
|
300
|
-
* const buttonRef = useRef<HTMLButtonElement>(null);
|
|
301
|
-
* useEventListener('click', handleClick, buttonRef);
|
|
302
|
-
*
|
|
303
|
-
* @example
|
|
304
|
-
* // With options
|
|
305
|
-
* useEventListener('scroll', handleScroll, window, { passive: true });
|
|
306
|
-
*/
|
|
307
|
-
export function useEventListener<K extends keyof WindowEventMap>(
|
|
308
|
-
eventName: K,
|
|
309
|
-
handler: (event: WindowEventMap[K]) => void,
|
|
310
|
-
element?: Window,
|
|
311
|
-
options?: AddEventListenerOptions | boolean,
|
|
312
|
-
): void;
|
|
313
|
-
export function useEventListener<
|
|
314
|
-
K extends keyof HTMLElementEventMap,
|
|
315
|
-
T extends HTMLElement = HTMLDivElement,
|
|
316
|
-
>(
|
|
317
|
-
eventName: K,
|
|
318
|
-
handler: (event: HTMLElementEventMap[K]) => void,
|
|
319
|
-
element: React.RefObject<null | T>,
|
|
320
|
-
options?: AddEventListenerOptions | boolean,
|
|
321
|
-
): void;
|
|
322
|
-
export function useEventListener<K extends keyof DocumentEventMap>(
|
|
323
|
-
eventName: K,
|
|
324
|
-
handler: (event: DocumentEventMap[K]) => void,
|
|
325
|
-
element: Document,
|
|
326
|
-
options?: AddEventListenerOptions | boolean,
|
|
327
|
-
): void;
|
|
328
|
-
export function useEventListener<
|
|
329
|
-
KW extends keyof WindowEventMap,
|
|
330
|
-
KH extends keyof HTMLElementEventMap,
|
|
331
|
-
KD extends keyof DocumentEventMap,
|
|
332
|
-
T extends HTMLElement = HTMLElement,
|
|
333
|
-
>(
|
|
334
|
-
eventName: KD | KH | KW,
|
|
335
|
-
handler: (
|
|
336
|
-
event:
|
|
337
|
-
| DocumentEventMap[KD]
|
|
338
|
-
| Event
|
|
339
|
-
| HTMLElementEventMap[KH]
|
|
340
|
-
| WindowEventMap[KW],
|
|
341
|
-
) => void,
|
|
342
|
-
element?: Document | React.RefObject<null | T> | Window,
|
|
343
|
-
options?: AddEventListenerOptions | boolean,
|
|
344
|
-
): void {
|
|
345
|
-
const savedHandler = useRef(handler);
|
|
346
|
-
|
|
347
|
-
useEffect(() => {
|
|
348
|
-
savedHandler.current = handler;
|
|
349
|
-
}, [handler]);
|
|
350
|
-
|
|
351
|
-
useEffect(() => {
|
|
352
|
-
let targetElement: Document | Element | null | Window;
|
|
353
|
-
|
|
354
|
-
if (element === undefined) {
|
|
355
|
-
targetElement = window;
|
|
356
|
-
} else if (element instanceof Document || element instanceof Window) {
|
|
357
|
-
targetElement = element;
|
|
358
|
-
} else {
|
|
359
|
-
targetElement = element.current;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
if (!targetElement?.addEventListener) {
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
const eventListener: typeof handler = (event) => {
|
|
367
|
-
savedHandler.current(event);
|
|
368
|
-
};
|
|
369
|
-
|
|
370
|
-
targetElement.addEventListener(eventName, eventListener, options);
|
|
371
|
-
|
|
372
|
-
return () => {
|
|
373
|
-
targetElement.removeEventListener(eventName, eventListener, options);
|
|
374
|
-
};
|
|
375
|
-
}, [eventName, element, options]);
|
|
376
|
-
}
|
|
377
|
-
`,
|
|
378
|
-
useFetch: `import { useCallback, useEffect, useRef, useState } from "react";
|
|
379
|
-
|
|
380
|
-
export interface UseFetchOptions<T> {
|
|
381
|
-
/** Whether to fetch immediately on mount (default: true) */
|
|
382
|
-
immediate?: boolean;
|
|
383
|
-
/** Initial data before fetch completes */
|
|
384
|
-
initialData?: T;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
export interface UseFetchResult<T> {
|
|
388
|
-
/** The fetched data, or undefined if not yet loaded */
|
|
389
|
-
data: T | undefined;
|
|
390
|
-
/** Error object if the fetch failed */
|
|
391
|
-
error: Error | null;
|
|
392
|
-
/** Whether a fetch is currently in progress */
|
|
393
|
-
isLoading: boolean;
|
|
394
|
-
/** Function to manually trigger a refetch */
|
|
395
|
-
refetch: () => Promise<void>;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
/**
|
|
399
|
-
* Fetch data from a URL with loading and error states.
|
|
400
|
-
*
|
|
401
|
-
* @param url - The URL to fetch data from
|
|
402
|
-
* @param options - Configuration options
|
|
403
|
-
* @returns Object containing data, loading state, error, and refetch function
|
|
404
|
-
*
|
|
405
|
-
* @example
|
|
406
|
-
* const { data, isLoading, error, refetch } = useFetch<User[]>('/api/users');
|
|
407
|
-
*
|
|
408
|
-
* if (isLoading) return <Spinner />;
|
|
409
|
-
* if (error) return <Error message={error.message} />;
|
|
410
|
-
* return <UserList users={data} />;
|
|
411
|
-
*
|
|
412
|
-
* @example
|
|
413
|
-
* // With initial data and manual fetch
|
|
414
|
-
* const { data, refetch } = useFetch<User>('/api/user', {
|
|
415
|
-
* initialData: { name: 'Loading...' },
|
|
416
|
-
* immediate: false,
|
|
417
|
-
* });
|
|
418
|
-
*/
|
|
419
|
-
export function useFetch<T>(
|
|
420
|
-
url: string,
|
|
421
|
-
options: UseFetchOptions<T> = {},
|
|
422
|
-
): UseFetchResult<T> {
|
|
423
|
-
const { immediate = true, initialData } = options;
|
|
424
|
-
|
|
425
|
-
const [data, setData] = useState<T | undefined>(initialData);
|
|
426
|
-
const [error, setError] = useState<Error | null>(null);
|
|
427
|
-
const [isLoading, setIsLoading] = useState(immediate);
|
|
428
|
-
|
|
429
|
-
const abortControllerRef = useRef<AbortController | null>(null);
|
|
430
|
-
|
|
431
|
-
const fetchData = useCallback(async () => {
|
|
432
|
-
// Cancel any in-flight request
|
|
433
|
-
abortControllerRef.current?.abort();
|
|
434
|
-
abortControllerRef.current = new AbortController();
|
|
435
|
-
|
|
436
|
-
setIsLoading(true);
|
|
437
|
-
setError(null);
|
|
438
|
-
|
|
439
|
-
try {
|
|
440
|
-
const response = await fetch(url, {
|
|
441
|
-
signal: abortControllerRef.current.signal,
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
if (!response.ok) {
|
|
445
|
-
throw new Error(\`HTTP error! status: \${String(response.status)}\`);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
const result = (await response.json()) as T;
|
|
449
|
-
setData(result);
|
|
450
|
-
} catch (err) {
|
|
451
|
-
if (err instanceof Error && err.name !== "AbortError") {
|
|
452
|
-
setError(err);
|
|
453
|
-
}
|
|
454
|
-
} finally {
|
|
455
|
-
setIsLoading(false);
|
|
456
|
-
}
|
|
457
|
-
}, [url]);
|
|
458
|
-
|
|
459
|
-
useEffect(() => {
|
|
460
|
-
if (immediate) {
|
|
461
|
-
void fetchData();
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
return () => {
|
|
465
|
-
abortControllerRef.current?.abort();
|
|
466
|
-
};
|
|
467
|
-
}, [fetchData, immediate]);
|
|
468
|
-
|
|
469
|
-
return { data, error, isLoading, refetch: fetchData };
|
|
470
|
-
}
|
|
471
|
-
`,
|
|
472
|
-
useHover: `import { useCallback, useRef, useState } from "react";
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* Tracks mouse hover state on a DOM element via ref.
|
|
476
|
-
*
|
|
477
|
-
* @returns Tuple of [isHovered, ref]
|
|
478
|
-
*
|
|
479
|
-
* @example
|
|
480
|
-
* const [isHovered, ref] = useHover();
|
|
481
|
-
*
|
|
482
|
-
* return (
|
|
483
|
-
* <div
|
|
484
|
-
* ref={ref}
|
|
485
|
-
* style={{
|
|
486
|
-
* backgroundColor: isHovered ? "blue" : "gray",
|
|
487
|
-
* }}
|
|
488
|
-
* >
|
|
489
|
-
* Hover me!
|
|
490
|
-
* </div>
|
|
491
|
-
* );
|
|
492
|
-
*/
|
|
493
|
-
export function useHover<T extends HTMLElement = HTMLElement>(): [
|
|
494
|
-
boolean,
|
|
495
|
-
React.RefObject<T>,
|
|
496
|
-
] {
|
|
497
|
-
const ref = useRef<T>(null);
|
|
498
|
-
const [isHovered, setIsHovered] = useState(false);
|
|
499
|
-
|
|
500
|
-
const handleMouseEnter = useCallback(() => {
|
|
501
|
-
setIsHovered(true);
|
|
502
|
-
}, []);
|
|
503
|
-
|
|
504
|
-
const handleMouseLeave = useCallback(() => {
|
|
505
|
-
setIsHovered(false);
|
|
506
|
-
}, []);
|
|
507
|
-
|
|
508
|
-
// Attach event listeners to the ref
|
|
509
|
-
const setRef = useCallback(
|
|
510
|
-
(element: null | T) => {
|
|
511
|
-
if (ref.current) {
|
|
512
|
-
ref.current.removeEventListener("mouseenter", handleMouseEnter);
|
|
513
|
-
ref.current.removeEventListener("mouseleave", handleMouseLeave);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
if (element) {
|
|
517
|
-
element.addEventListener("mouseenter", handleMouseEnter);
|
|
518
|
-
element.addEventListener("mouseleave", handleMouseLeave);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
ref.current = element;
|
|
522
|
-
},
|
|
523
|
-
[handleMouseEnter, handleMouseLeave],
|
|
524
|
-
);
|
|
525
|
-
|
|
526
|
-
// Return a proxy ref that updates the internal ref
|
|
527
|
-
return [
|
|
528
|
-
isHovered,
|
|
529
|
-
{
|
|
530
|
-
get current() {
|
|
531
|
-
return ref.current;
|
|
532
|
-
},
|
|
533
|
-
set current(element: null | T) {
|
|
534
|
-
setRef(element);
|
|
535
|
-
},
|
|
536
|
-
} as React.RefObject<T>,
|
|
537
|
-
];
|
|
538
|
-
}
|
|
539
|
-
`,
|
|
540
|
-
useIntersectionObserver: `import { useEffect, useRef, useState } from "react";
|
|
541
|
-
|
|
542
|
-
export interface UseIntersectionObserverOptions {
|
|
543
|
-
/** Whether to stop observing after the first intersection (default: false) */
|
|
544
|
-
once?: boolean;
|
|
545
|
-
/** The element used as the viewport for checking visibility (default: browser viewport) */
|
|
546
|
-
root?: Element | null;
|
|
547
|
-
/** Margin around the root element (e.g., "10px 20px 30px 40px") */
|
|
548
|
-
rootMargin?: string;
|
|
549
|
-
/** A number or array of numbers indicating at what percentage of visibility the callback should trigger */
|
|
550
|
-
threshold?: number | number[];
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
export interface UseIntersectionObserverResult {
|
|
554
|
-
/** The current intersection observer entry */
|
|
555
|
-
entry: IntersectionObserverEntry | null;
|
|
556
|
-
/** Whether the element is currently intersecting */
|
|
557
|
-
isIntersecting: boolean;
|
|
558
|
-
/** Ref to attach to the element to observe */
|
|
559
|
-
ref: React.RefObject<HTMLElement | null>;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
/**
|
|
563
|
-
* Track the visibility of a DOM element within the viewport using IntersectionObserver.
|
|
564
|
-
*
|
|
565
|
-
* @param options - IntersectionObserver configuration options
|
|
566
|
-
* @returns Object containing ref, entry, and isIntersecting state
|
|
567
|
-
*
|
|
568
|
-
* @example
|
|
569
|
-
* // Basic usage - lazy load an image
|
|
570
|
-
* const { ref, isIntersecting } = useIntersectionObserver();
|
|
571
|
-
*
|
|
572
|
-
* return (
|
|
573
|
-
* <div ref={ref}>
|
|
574
|
-
* {isIntersecting && <img src="large-image.jpg" />}
|
|
575
|
-
* </div>
|
|
576
|
-
* );
|
|
577
|
-
*
|
|
578
|
-
* @example
|
|
579
|
-
* // Infinite scroll with threshold
|
|
580
|
-
* const { ref, isIntersecting } = useIntersectionObserver({
|
|
581
|
-
* threshold: 0.5,
|
|
582
|
-
* rootMargin: '100px',
|
|
583
|
-
* });
|
|
584
|
-
*
|
|
585
|
-
* useEffect(() => {
|
|
586
|
-
* if (isIntersecting) loadMoreItems();
|
|
587
|
-
* }, [isIntersecting]);
|
|
588
|
-
*/
|
|
589
|
-
export function useIntersectionObserver(
|
|
590
|
-
options: UseIntersectionObserverOptions = {},
|
|
591
|
-
): UseIntersectionObserverResult {
|
|
592
|
-
const {
|
|
593
|
-
once = false,
|
|
594
|
-
root = null,
|
|
595
|
-
rootMargin = "0px",
|
|
596
|
-
threshold = 0,
|
|
597
|
-
} = options;
|
|
598
|
-
|
|
599
|
-
const ref = useRef<HTMLElement | null>(null);
|
|
600
|
-
const [entry, setEntry] = useState<IntersectionObserverEntry | null>(null);
|
|
601
|
-
const hasTriggered = useRef(false);
|
|
602
|
-
|
|
603
|
-
useEffect(() => {
|
|
604
|
-
const element = ref.current;
|
|
605
|
-
|
|
606
|
-
if (!element || (once && hasTriggered.current)) {
|
|
607
|
-
return;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
if (typeof IntersectionObserver === "undefined") {
|
|
611
|
-
return;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
const observer = new IntersectionObserver(
|
|
615
|
-
([observerEntry]) => {
|
|
616
|
-
if (!observerEntry) {
|
|
617
|
-
return;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
setEntry(observerEntry);
|
|
621
|
-
|
|
622
|
-
if (once && observerEntry.isIntersecting) {
|
|
623
|
-
hasTriggered.current = true;
|
|
624
|
-
observer.disconnect();
|
|
625
|
-
}
|
|
626
|
-
},
|
|
627
|
-
{ root, rootMargin, threshold },
|
|
628
|
-
);
|
|
629
|
-
|
|
630
|
-
observer.observe(element);
|
|
631
|
-
|
|
632
|
-
return () => {
|
|
633
|
-
observer.disconnect();
|
|
634
|
-
};
|
|
635
|
-
}, [root, rootMargin, threshold, once]);
|
|
636
|
-
|
|
637
|
-
return {
|
|
638
|
-
entry,
|
|
639
|
-
isIntersecting: entry?.isIntersecting ?? false,
|
|
640
|
-
ref,
|
|
641
|
-
};
|
|
642
|
-
}
|
|
643
|
-
`,
|
|
644
|
-
useInterval: `import { useEffect } from "react";
|
|
645
|
-
|
|
646
|
-
/**
|
|
647
|
-
* Calls a callback at specified intervals.
|
|
648
|
-
*
|
|
649
|
-
* @param callback - Function to call on each interval
|
|
650
|
-
* @param delay - Interval delay in milliseconds (null to pause)
|
|
651
|
-
*
|
|
652
|
-
* @example
|
|
653
|
-
* useInterval(() => {
|
|
654
|
-
* setTime(new Date());
|
|
655
|
-
* }, 1000);
|
|
656
|
-
*
|
|
657
|
-
* @example
|
|
658
|
-
* // Pause interval by passing null
|
|
659
|
-
* useInterval(callback, isPaused ? null : 1000);
|
|
660
|
-
*/
|
|
661
|
-
export function useInterval(callback: () => void, delay: null | number): void {
|
|
662
|
-
useEffect(() => {
|
|
663
|
-
if (delay === null) return;
|
|
664
|
-
|
|
665
|
-
const interval = setInterval(callback, delay);
|
|
666
|
-
return () => {
|
|
667
|
-
clearInterval(interval);
|
|
668
|
-
};
|
|
669
|
-
}, [callback, delay]);
|
|
670
|
-
}
|
|
671
|
-
`,
|
|
672
|
-
useIsClient: `import { useEffect, useState } from "react";
|
|
673
|
-
|
|
674
|
-
/**
|
|
675
|
-
* Determine if the code is running on the client-side.
|
|
676
|
-
* Useful for SSR-safe code that needs to access browser APIs.
|
|
677
|
-
*
|
|
678
|
-
* @returns true if running on client, false during SSR
|
|
679
|
-
*
|
|
680
|
-
* @example
|
|
681
|
-
* const isClient = useIsClient();
|
|
682
|
-
*
|
|
683
|
-
* if (!isClient) {
|
|
684
|
-
* return <div>Loading...</div>;
|
|
685
|
-
* }
|
|
686
|
-
*
|
|
687
|
-
* // Safe to use browser APIs
|
|
688
|
-
* return <div>Window width: {window.innerWidth}</div>;
|
|
689
|
-
*
|
|
690
|
-
* @example
|
|
691
|
-
* // Conditionally render client-only components
|
|
692
|
-
* const isClient = useIsClient();
|
|
693
|
-
*
|
|
694
|
-
* return (
|
|
695
|
-
* <div>
|
|
696
|
-
* <Header />
|
|
697
|
-
* {isClient && <ClientOnlyMap />}
|
|
698
|
-
* <Footer />
|
|
699
|
-
* </div>
|
|
700
|
-
* );
|
|
701
|
-
*/
|
|
702
|
-
export function useIsClient(): boolean {
|
|
703
|
-
const [isClient, setIsClient] = useState(false);
|
|
704
|
-
|
|
705
|
-
useEffect(() => {
|
|
706
|
-
setIsClient(true);
|
|
707
|
-
}, []);
|
|
708
|
-
|
|
709
|
-
return isClient;
|
|
710
|
-
}
|
|
711
|
-
`,
|
|
712
|
-
useKeyPress: `import { useEffect, useState } from "react";
|
|
713
|
-
|
|
714
|
-
/**
|
|
715
|
-
* Detects if a specific keyboard key is currently pressed.
|
|
716
|
-
*
|
|
717
|
-
* @param targetKey - The key to detect (e.g., "Enter", "ArrowUp", " " for space)
|
|
718
|
-
* @returns Whether the key is currently pressed
|
|
719
|
-
*
|
|
720
|
-
* @example
|
|
721
|
-
* const isEnterPressed = useKeyPress("Enter");
|
|
722
|
-
* const isArrowUpPressed = useKeyPress("ArrowUp");
|
|
723
|
-
*
|
|
724
|
-
* return (
|
|
725
|
-
* <div>
|
|
726
|
-
* <p>Enter pressed: {isEnterPressed ? "Yes" : "No"}</p>
|
|
727
|
-
* <p>Arrow Up pressed: {isArrowUpPressed ? "Yes" : "No"}</p>
|
|
728
|
-
* </div>
|
|
729
|
-
* );
|
|
730
|
-
*/
|
|
731
|
-
export function useKeyPress(targetKey: string): boolean {
|
|
732
|
-
const [isKeyPressed, setIsKeyPressed] = useState(false);
|
|
733
|
-
|
|
734
|
-
useEffect(() => {
|
|
735
|
-
const handleKeyDown = (event: KeyboardEvent) => {
|
|
736
|
-
if (event.key === targetKey) {
|
|
737
|
-
setIsKeyPressed(true);
|
|
738
|
-
}
|
|
739
|
-
};
|
|
740
|
-
|
|
741
|
-
const handleKeyUp = (event: KeyboardEvent) => {
|
|
742
|
-
if (event.key === targetKey) {
|
|
743
|
-
setIsKeyPressed(false);
|
|
744
|
-
}
|
|
745
|
-
};
|
|
746
|
-
|
|
747
|
-
window.addEventListener("keydown", handleKeyDown);
|
|
748
|
-
window.addEventListener("keyup", handleKeyUp);
|
|
749
|
-
|
|
750
|
-
return () => {
|
|
751
|
-
window.removeEventListener("keydown", handleKeyDown);
|
|
752
|
-
window.removeEventListener("keyup", handleKeyUp);
|
|
753
|
-
};
|
|
754
|
-
}, [targetKey]);
|
|
755
|
-
|
|
756
|
-
return isKeyPressed;
|
|
757
|
-
}
|
|
758
|
-
`,
|
|
759
|
-
useLocalStorage: `import { useCallback, useEffect, useState } from "react";
|
|
760
|
-
|
|
761
|
-
/**
|
|
762
|
-
* Syncs state with localStorage, persisting across browser sessions.
|
|
763
|
-
*
|
|
764
|
-
* @param key - The localStorage key
|
|
765
|
-
* @param initialValue - The initial value (used if no stored value exists)
|
|
766
|
-
* @returns A tuple of [value, setValue, removeValue]
|
|
767
|
-
*
|
|
768
|
-
* @example
|
|
769
|
-
* const [theme, setTheme, removeTheme] = useLocalStorage("theme", "light");
|
|
770
|
-
*
|
|
771
|
-
* // Update the theme (automatically persisted)
|
|
772
|
-
* setTheme("dark");
|
|
773
|
-
*
|
|
774
|
-
* // Remove from localStorage
|
|
775
|
-
* removeTheme();
|
|
776
|
-
*/
|
|
777
|
-
export function useLocalStorage<T>(
|
|
778
|
-
key: string,
|
|
779
|
-
initialValue: T,
|
|
780
|
-
): [T, (value: ((prev: T) => T) | T) => void, () => void] {
|
|
781
|
-
// Get initial value from localStorage or use provided initial value
|
|
782
|
-
const [storedValue, setStoredValue] = useState<T>(() => {
|
|
783
|
-
if (typeof window === "undefined") {
|
|
784
|
-
return initialValue;
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
try {
|
|
788
|
-
const item = window.localStorage.getItem(key);
|
|
789
|
-
return item ? (JSON.parse(item) as T) : initialValue;
|
|
790
|
-
} catch (error) {
|
|
791
|
-
console.warn(\`Error reading localStorage key "\${key}":\`, error);
|
|
792
|
-
return initialValue;
|
|
793
|
-
}
|
|
794
|
-
});
|
|
795
|
-
|
|
796
|
-
// Update localStorage when value changes
|
|
797
|
-
const setValue = useCallback(
|
|
798
|
-
(value: ((prev: T) => T) | T) => {
|
|
799
|
-
try {
|
|
800
|
-
setStoredValue((prev) => {
|
|
801
|
-
const valueToStore = value instanceof Function ? value(prev) : value;
|
|
802
|
-
if (typeof window !== "undefined") {
|
|
803
|
-
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
|
804
|
-
}
|
|
805
|
-
return valueToStore;
|
|
806
|
-
});
|
|
807
|
-
} catch (error) {
|
|
808
|
-
console.warn(\`Error setting localStorage key "\${key}":\`, error);
|
|
809
|
-
}
|
|
810
|
-
},
|
|
811
|
-
[key],
|
|
812
|
-
);
|
|
813
|
-
|
|
814
|
-
// Remove from localStorage
|
|
815
|
-
const removeValue = useCallback(() => {
|
|
816
|
-
try {
|
|
817
|
-
if (typeof window !== "undefined") {
|
|
818
|
-
window.localStorage.removeItem(key);
|
|
819
|
-
}
|
|
820
|
-
setStoredValue(initialValue);
|
|
821
|
-
} catch (error) {
|
|
822
|
-
console.warn(\`Error removing localStorage key "\${key}":\`, error);
|
|
823
|
-
}
|
|
824
|
-
}, [key, initialValue]);
|
|
825
|
-
|
|
826
|
-
// Listen for changes in other tabs/windows
|
|
827
|
-
useEffect(() => {
|
|
828
|
-
const handleStorageChange = (event: StorageEvent) => {
|
|
829
|
-
if (event.key === key && event.newValue !== null) {
|
|
830
|
-
try {
|
|
831
|
-
setStoredValue(JSON.parse(event.newValue) as T);
|
|
832
|
-
} catch (error) {
|
|
833
|
-
console.warn(\`Error parsing localStorage key "\${key}":\`, error);
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
};
|
|
837
|
-
|
|
838
|
-
window.addEventListener("storage", handleStorageChange);
|
|
839
|
-
return () => {
|
|
840
|
-
window.removeEventListener("storage", handleStorageChange);
|
|
841
|
-
};
|
|
842
|
-
}, [key]);
|
|
843
|
-
|
|
844
|
-
return [storedValue, setValue, removeValue];
|
|
845
|
-
}
|
|
846
|
-
`,
|
|
847
|
-
useLockBodyScroll: `import { useEffect, useRef } from "react";
|
|
848
|
-
|
|
849
|
-
/**
|
|
850
|
-
* Temporarily disable scrolling on the document body.
|
|
851
|
-
* Useful for modals, drawers, and other overlays.
|
|
852
|
-
*
|
|
853
|
-
* @example
|
|
854
|
-
* // Lock body scroll when modal is open
|
|
855
|
-
* function Modal({ isOpen }) {
|
|
856
|
-
* useLockBodyScroll(isOpen);
|
|
857
|
-
*
|
|
858
|
-
* if (!isOpen) return null;
|
|
859
|
-
* return <div className="modal">...</div>;
|
|
860
|
-
* }
|
|
861
|
-
*
|
|
862
|
-
* @example
|
|
863
|
-
* // Always lock when component is mounted
|
|
864
|
-
* function FullscreenOverlay() {
|
|
865
|
-
* useLockBodyScroll();
|
|
866
|
-
* return <div className="overlay">...</div>;
|
|
867
|
-
* }
|
|
868
|
-
*/
|
|
869
|
-
export function useLockBodyScroll(lock = true): void {
|
|
870
|
-
const originalStyle = useRef<string | undefined>(undefined);
|
|
871
|
-
|
|
872
|
-
useEffect(() => {
|
|
873
|
-
if (typeof document === "undefined") {
|
|
874
|
-
return;
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
if (!lock) {
|
|
878
|
-
return;
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
// Store the original overflow style
|
|
882
|
-
originalStyle.current = document.body.style.overflow;
|
|
883
|
-
|
|
884
|
-
// Lock the body scroll
|
|
885
|
-
document.body.style.overflow = "hidden";
|
|
886
|
-
|
|
887
|
-
return () => {
|
|
888
|
-
// Restore the original overflow style
|
|
889
|
-
document.body.style.overflow = originalStyle.current ?? "";
|
|
890
|
-
};
|
|
891
|
-
}, [lock]);
|
|
892
|
-
}
|
|
893
|
-
`,
|
|
894
|
-
useMeasure: `import { useCallback, useEffect, useRef, useState } from "react";
|
|
895
|
-
|
|
896
|
-
export interface UseMeasureRect {
|
|
897
|
-
/** Bottom position relative to viewport */
|
|
898
|
-
bottom: number;
|
|
899
|
-
/** Element height */
|
|
900
|
-
height: number;
|
|
901
|
-
/** Left position relative to viewport */
|
|
902
|
-
left: number;
|
|
903
|
-
/** Right position relative to viewport */
|
|
904
|
-
right: number;
|
|
905
|
-
/** Top position relative to viewport */
|
|
906
|
-
top: number;
|
|
907
|
-
/** Element width */
|
|
908
|
-
width: number;
|
|
909
|
-
/** X position (same as left) */
|
|
910
|
-
x: number;
|
|
911
|
-
/** Y position (same as top) */
|
|
912
|
-
y: number;
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
export interface UseMeasureResult<T extends Element> {
|
|
916
|
-
/** The measured dimensions of the element */
|
|
917
|
-
rect: UseMeasureRect;
|
|
918
|
-
/** Ref to attach to the element to measure */
|
|
919
|
-
ref: React.RefCallback<T>;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
const defaultRect: UseMeasureRect = {
|
|
923
|
-
bottom: 0,
|
|
924
|
-
height: 0,
|
|
925
|
-
left: 0,
|
|
926
|
-
right: 0,
|
|
927
|
-
top: 0,
|
|
928
|
-
width: 0,
|
|
929
|
-
x: 0,
|
|
930
|
-
y: 0,
|
|
931
|
-
};
|
|
932
|
-
|
|
933
|
-
/**
|
|
934
|
-
* Measure the dimensions of a DOM element using ResizeObserver.
|
|
935
|
-
*
|
|
936
|
-
* @returns Object containing ref callback and rect measurements
|
|
937
|
-
*
|
|
938
|
-
* @example
|
|
939
|
-
* const { ref, rect } = useMeasure<HTMLDivElement>();
|
|
940
|
-
*
|
|
941
|
-
* return (
|
|
942
|
-
* <div ref={ref}>
|
|
943
|
-
* <p>Width: {rect.width}px</p>
|
|
944
|
-
* <p>Height: {rect.height}px</p>
|
|
945
|
-
* </div>
|
|
946
|
-
* );
|
|
947
|
-
*
|
|
948
|
-
* @example
|
|
949
|
-
* // Responsive component
|
|
950
|
-
* const { ref, rect } = useMeasure<HTMLDivElement>();
|
|
951
|
-
* const isCompact = rect.width < 400;
|
|
952
|
-
*
|
|
953
|
-
* return (
|
|
954
|
-
* <nav ref={ref} className={isCompact ? 'compact' : 'full'}>
|
|
955
|
-
* {isCompact ? <MobileMenu /> : <DesktopMenu />}
|
|
956
|
-
* </nav>
|
|
957
|
-
* );
|
|
958
|
-
*/
|
|
959
|
-
export function useMeasure<
|
|
960
|
-
T extends Element = HTMLDivElement,
|
|
961
|
-
>(): UseMeasureResult<T> {
|
|
962
|
-
const [element, setElement] = useState<null | T>(null);
|
|
963
|
-
const [rect, setRect] = useState<UseMeasureRect>(defaultRect);
|
|
964
|
-
|
|
965
|
-
const observerRef = useRef<null | ResizeObserver>(null);
|
|
966
|
-
|
|
967
|
-
const ref = useCallback((node: null | T) => {
|
|
968
|
-
setElement(node);
|
|
969
|
-
}, []);
|
|
970
|
-
|
|
971
|
-
useEffect(() => {
|
|
972
|
-
if (!element) {
|
|
973
|
-
return;
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
if (typeof ResizeObserver === "undefined") {
|
|
977
|
-
// Fallback: get initial dimensions without observing changes
|
|
978
|
-
const boundingRect = element.getBoundingClientRect();
|
|
979
|
-
setRect({
|
|
980
|
-
bottom: boundingRect.bottom,
|
|
981
|
-
height: boundingRect.height,
|
|
982
|
-
left: boundingRect.left,
|
|
983
|
-
right: boundingRect.right,
|
|
984
|
-
top: boundingRect.top,
|
|
985
|
-
width: boundingRect.width,
|
|
986
|
-
x: boundingRect.x,
|
|
987
|
-
y: boundingRect.y,
|
|
988
|
-
});
|
|
989
|
-
return;
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
observerRef.current = new ResizeObserver(([entry]) => {
|
|
993
|
-
if (entry) {
|
|
994
|
-
const boundingRect = entry.target.getBoundingClientRect();
|
|
995
|
-
setRect({
|
|
996
|
-
bottom: boundingRect.bottom,
|
|
997
|
-
height: boundingRect.height,
|
|
998
|
-
left: boundingRect.left,
|
|
999
|
-
right: boundingRect.right,
|
|
1000
|
-
top: boundingRect.top,
|
|
1001
|
-
width: boundingRect.width,
|
|
1002
|
-
x: boundingRect.x,
|
|
1003
|
-
y: boundingRect.y,
|
|
1004
|
-
});
|
|
1005
|
-
}
|
|
1006
|
-
});
|
|
1007
|
-
|
|
1008
|
-
observerRef.current.observe(element);
|
|
1009
|
-
|
|
1010
|
-
return () => {
|
|
1011
|
-
observerRef.current?.disconnect();
|
|
1012
|
-
};
|
|
1013
|
-
}, [element]);
|
|
1014
|
-
|
|
1015
|
-
return { rect, ref };
|
|
1016
|
-
}
|
|
1017
|
-
`,
|
|
1018
|
-
useMedia: `import { useEffect, useState } from "react";
|
|
1019
|
-
|
|
1020
|
-
/**
|
|
1021
|
-
* Reacts to CSS media query changes.
|
|
1022
|
-
*
|
|
1023
|
-
* @param query - CSS media query string (e.g., "(max-width: 768px)")
|
|
1024
|
-
* @returns Whether the media query matches
|
|
1025
|
-
*
|
|
1026
|
-
* @example
|
|
1027
|
-
* const isMobile = useMedia("(max-width: 768px)");
|
|
1028
|
-
* const isDarkMode = useMedia("(prefers-color-scheme: dark)");
|
|
1029
|
-
*
|
|
1030
|
-
* return (
|
|
1031
|
-
* <div>
|
|
1032
|
-
* <p>Is mobile: {isMobile ? "Yes" : "No"}</p>
|
|
1033
|
-
* <p>Dark mode: {isDarkMode ? "Yes" : "No"}</p>
|
|
1034
|
-
* </div>
|
|
1035
|
-
* );
|
|
1036
|
-
*/
|
|
1037
|
-
export function useMedia(query: string): boolean {
|
|
1038
|
-
const [matches, setMatches] = useState(false);
|
|
1039
|
-
|
|
1040
|
-
useEffect(() => {
|
|
1041
|
-
// Check if window is defined (SSR safety)
|
|
1042
|
-
if (typeof window === "undefined") {
|
|
1043
|
-
return undefined;
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
try {
|
|
1047
|
-
const mediaQueryList = window.matchMedia(query);
|
|
1048
|
-
|
|
1049
|
-
// Set initial value
|
|
1050
|
-
setMatches(mediaQueryList.matches);
|
|
1051
|
-
|
|
1052
|
-
// Create listener function
|
|
1053
|
-
const handleChange = (e: MediaQueryListEvent) => {
|
|
1054
|
-
setMatches(e.matches);
|
|
1055
|
-
};
|
|
1056
|
-
|
|
1057
|
-
// Modern browsers use addEventListener
|
|
1058
|
-
mediaQueryList.addEventListener("change", handleChange);
|
|
1059
|
-
return () => {
|
|
1060
|
-
mediaQueryList.removeEventListener("change", handleChange);
|
|
1061
|
-
};
|
|
1062
|
-
} catch (error) {
|
|
1063
|
-
console.warn(\`Invalid media query: "\${query}"\`, error);
|
|
1064
|
-
return undefined;
|
|
1065
|
-
}
|
|
1066
|
-
}, [query]);
|
|
1067
|
-
|
|
1068
|
-
return matches;
|
|
1069
|
-
}
|
|
1070
|
-
`,
|
|
1071
|
-
useMount: `import { useEffect } from "react";
|
|
1072
|
-
|
|
1073
|
-
/**
|
|
1074
|
-
* Calls a callback on component mount.
|
|
1075
|
-
*
|
|
1076
|
-
* @param callback - Function to call on mount
|
|
1077
|
-
*
|
|
1078
|
-
* @example
|
|
1079
|
-
* useMount(() => {
|
|
1080
|
-
* console.log("Component mounted");
|
|
1081
|
-
* // Initialize resources
|
|
1082
|
-
* });
|
|
1083
|
-
*/
|
|
1084
|
-
export function useMount(callback: () => void): void {
|
|
1085
|
-
useEffect(callback, []);
|
|
1086
|
-
}
|
|
1087
|
-
`,
|
|
1088
|
-
usePrevious: `import { useEffect, useRef } from "react";
|
|
1089
|
-
|
|
1090
|
-
/**
|
|
1091
|
-
* Tracks the previous value or prop.
|
|
1092
|
-
*
|
|
1093
|
-
* @param value - The current value to track
|
|
1094
|
-
* @returns The previous value from the last render
|
|
1095
|
-
*
|
|
1096
|
-
* @example
|
|
1097
|
-
* const [count, setCount] = useState(0);
|
|
1098
|
-
* const prevCount = usePrevious(count);
|
|
1099
|
-
*
|
|
1100
|
-
* useEffect(() => {
|
|
1101
|
-
* console.log(\`Current: \${count}, Previous: \${prevCount}\`);
|
|
1102
|
-
* }, [count, prevCount]);
|
|
1103
|
-
*/
|
|
1104
|
-
export function usePrevious<T>(value: T): T | undefined {
|
|
1105
|
-
const ref = useRef<T | undefined>(undefined);
|
|
1106
|
-
|
|
1107
|
-
useEffect(() => {
|
|
1108
|
-
ref.current = value;
|
|
1109
|
-
}, [value]);
|
|
1110
|
-
|
|
1111
|
-
return ref.current;
|
|
1112
|
-
}
|
|
1113
|
-
`,
|
|
1114
|
-
useScroll: `import { useCallback, useEffect, useState } from "react";
|
|
1115
|
-
|
|
1116
|
-
interface ScrollPosition {
|
|
1117
|
-
x: number;
|
|
1118
|
-
y: number;
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
/**
|
|
1122
|
-
* Tracks scroll position of an element or the window.
|
|
1123
|
-
*
|
|
1124
|
-
* @param ref - Optional ref to an element. If not provided, tracks window scroll
|
|
1125
|
-
* @returns Object with x and y scroll positions
|
|
1126
|
-
*
|
|
1127
|
-
* @example
|
|
1128
|
-
* // Track window scroll
|
|
1129
|
-
* const scroll = useScroll();
|
|
1130
|
-
* console.log(scroll.x, scroll.y);
|
|
1131
|
-
*
|
|
1132
|
-
* @example
|
|
1133
|
-
* // Track element scroll
|
|
1134
|
-
* const elementRef = useRef<HTMLDivElement>(null);
|
|
1135
|
-
* const scroll = useScroll(elementRef);
|
|
1136
|
-
* return <div ref={elementRef} style={{ overflow: 'auto' }}>Content</div>;
|
|
1137
|
-
*/
|
|
1138
|
-
export function useScroll(
|
|
1139
|
-
ref?: React.RefObject<HTMLElement | null>,
|
|
1140
|
-
): ScrollPosition {
|
|
1141
|
-
const [scroll, setScroll] = useState<ScrollPosition>({ x: 0, y: 0 });
|
|
1142
|
-
|
|
1143
|
-
const handleScroll = useCallback(() => {
|
|
1144
|
-
if (ref?.current) {
|
|
1145
|
-
setScroll({
|
|
1146
|
-
x: ref.current.scrollLeft,
|
|
1147
|
-
y: ref.current.scrollTop,
|
|
1148
|
-
});
|
|
1149
|
-
} else if (typeof window !== "undefined") {
|
|
1150
|
-
setScroll({
|
|
1151
|
-
x: window.scrollX,
|
|
1152
|
-
y: window.scrollY,
|
|
1153
|
-
});
|
|
1154
|
-
}
|
|
1155
|
-
}, [ref]);
|
|
1156
|
-
|
|
1157
|
-
useEffect(() => {
|
|
1158
|
-
// Set initial scroll position
|
|
1159
|
-
handleScroll();
|
|
1160
|
-
|
|
1161
|
-
if (ref?.current) {
|
|
1162
|
-
// Listen to element scroll
|
|
1163
|
-
const target = ref.current;
|
|
1164
|
-
target.addEventListener("scroll", handleScroll);
|
|
1165
|
-
return () => {
|
|
1166
|
-
target.removeEventListener("scroll", handleScroll);
|
|
1167
|
-
};
|
|
1168
|
-
} else if (typeof window !== "undefined") {
|
|
1169
|
-
// Listen to window scroll
|
|
1170
|
-
window.addEventListener("scroll", handleScroll);
|
|
1171
|
-
return () => {
|
|
1172
|
-
window.removeEventListener("scroll", handleScroll);
|
|
1173
|
-
};
|
|
1174
|
-
}
|
|
1175
|
-
}, [ref, handleScroll]);
|
|
1176
|
-
|
|
1177
|
-
return scroll;
|
|
1178
|
-
}
|
|
1179
|
-
`,
|
|
1180
|
-
useSessionStorage: `import { useCallback, useState } from "react";
|
|
1181
|
-
|
|
1182
|
-
/**
|
|
1183
|
-
* Syncs state with sessionStorage, persisting only for the current session.
|
|
1184
|
-
*
|
|
1185
|
-
* @param key - The sessionStorage key
|
|
1186
|
-
* @param initialValue - The initial value (used if no stored value exists)
|
|
1187
|
-
* @returns A tuple of [value, setValue, removeValue]
|
|
1188
|
-
*
|
|
1189
|
-
* @example
|
|
1190
|
-
* const [sessionData, setSessionData, removeSessionData] = useSessionStorage("session", "default");
|
|
1191
|
-
*
|
|
1192
|
-
* // Update the session data (automatically persisted)
|
|
1193
|
-
* setSessionData("newData");
|
|
1194
|
-
*
|
|
1195
|
-
* // Remove from sessionStorage
|
|
1196
|
-
* removeSessionData();
|
|
1197
|
-
*/
|
|
1198
|
-
export function useSessionStorage<T>(
|
|
1199
|
-
key: string,
|
|
1200
|
-
initialValue: T,
|
|
1201
|
-
): [T, (value: ((prev: T) => T) | T) => void, () => void] {
|
|
1202
|
-
// Get initial value from sessionStorage or use provided initial value
|
|
1203
|
-
const [storedValue, setStoredValue] = useState<T>(() => {
|
|
1204
|
-
if (typeof window === "undefined") {
|
|
1205
|
-
return initialValue;
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
try {
|
|
1209
|
-
const item = window.sessionStorage.getItem(key);
|
|
1210
|
-
return item ? (JSON.parse(item) as T) : initialValue;
|
|
1211
|
-
} catch (error) {
|
|
1212
|
-
console.warn(\`Error reading sessionStorage key "\${key}":\`, error);
|
|
1213
|
-
return initialValue;
|
|
1214
|
-
}
|
|
1215
|
-
});
|
|
1216
|
-
|
|
1217
|
-
// Update sessionStorage when value changes
|
|
1218
|
-
const setValue = useCallback(
|
|
1219
|
-
(value: ((prev: T) => T) | T) => {
|
|
1220
|
-
try {
|
|
1221
|
-
setStoredValue((prev) => {
|
|
1222
|
-
const valueToStore = value instanceof Function ? value(prev) : value;
|
|
1223
|
-
if (typeof window !== "undefined") {
|
|
1224
|
-
window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
|
|
1225
|
-
}
|
|
1226
|
-
return valueToStore;
|
|
1227
|
-
});
|
|
1228
|
-
} catch (error) {
|
|
1229
|
-
console.warn(\`Error setting sessionStorage key "\${key}":\`, error);
|
|
1230
|
-
}
|
|
1231
|
-
},
|
|
1232
|
-
[key],
|
|
1233
|
-
);
|
|
1234
|
-
|
|
1235
|
-
// Remove from sessionStorage
|
|
1236
|
-
const removeValue = useCallback(() => {
|
|
1237
|
-
try {
|
|
1238
|
-
if (typeof window !== "undefined") {
|
|
1239
|
-
window.sessionStorage.removeItem(key);
|
|
1240
|
-
}
|
|
1241
|
-
setStoredValue(initialValue);
|
|
1242
|
-
} catch (error) {
|
|
1243
|
-
console.warn(\`Error removing sessionStorage key "\${key}":\`, error);
|
|
1244
|
-
}
|
|
1245
|
-
}, [key, initialValue]);
|
|
1246
|
-
|
|
1247
|
-
return [storedValue, setValue, removeValue];
|
|
1248
|
-
}
|
|
1249
|
-
`,
|
|
1250
|
-
useThrottle: `import { useEffect, useRef, useState } from "react";
|
|
1251
|
-
|
|
1252
|
-
/**
|
|
1253
|
-
* Throttles a value to update at most once per specified interval.
|
|
1254
|
-
*
|
|
1255
|
-
* @param value - The value to throttle
|
|
1256
|
-
* @param interval - The throttle interval in milliseconds (default: 500ms)
|
|
1257
|
-
* @returns The throttled value
|
|
1258
|
-
*
|
|
1259
|
-
* @example
|
|
1260
|
-
* const [position, setPosition] = useState({ x: 0, y: 0 });
|
|
1261
|
-
* const throttledPosition = useThrottle(position, 100);
|
|
1262
|
-
*
|
|
1263
|
-
* useEffect(() => {
|
|
1264
|
-
* // This effect runs at most every 100ms
|
|
1265
|
-
* updateCursor(throttledPosition);
|
|
1266
|
-
* }, [throttledPosition]);
|
|
1267
|
-
*/
|
|
1268
|
-
export function useThrottle<T>(value: T, interval = 500): T {
|
|
1269
|
-
const [throttledValue, setThrottledValue] = useState<T>(value);
|
|
1270
|
-
const lastUpdated = useRef<number>(Date.now());
|
|
1271
|
-
|
|
1272
|
-
useEffect(() => {
|
|
1273
|
-
const now = Date.now();
|
|
1274
|
-
const elapsed = now - lastUpdated.current;
|
|
1275
|
-
|
|
1276
|
-
if (elapsed >= interval) {
|
|
1277
|
-
lastUpdated.current = now;
|
|
1278
|
-
setThrottledValue(value);
|
|
1279
|
-
} else {
|
|
1280
|
-
const timer = setTimeout(() => {
|
|
1281
|
-
lastUpdated.current = Date.now();
|
|
1282
|
-
setThrottledValue(value);
|
|
1283
|
-
}, interval - elapsed);
|
|
1284
|
-
|
|
1285
|
-
return () => {
|
|
1286
|
-
clearTimeout(timer);
|
|
1287
|
-
};
|
|
1288
|
-
}
|
|
1289
|
-
}, [value, interval]);
|
|
1290
|
-
|
|
1291
|
-
return throttledValue;
|
|
1292
|
-
}
|
|
1293
|
-
`,
|
|
1294
|
-
useTimeout: `import { useEffect } from "react";
|
|
1295
|
-
|
|
1296
|
-
/**
|
|
1297
|
-
* Calls a callback after a timeout.
|
|
1298
|
-
*
|
|
1299
|
-
* @param callback - Function to call after timeout
|
|
1300
|
-
* @param delay - Timeout delay in milliseconds (null to disable)
|
|
1301
|
-
*
|
|
1302
|
-
* @example
|
|
1303
|
-
* useTimeout(() => {
|
|
1304
|
-
* console.log("Timeout completed");
|
|
1305
|
-
* }, 2000);
|
|
1306
|
-
*
|
|
1307
|
-
* @example
|
|
1308
|
-
* // Disable timeout by passing null
|
|
1309
|
-
* useTimeout(() => {
|
|
1310
|
-
* console.log("This won't run");
|
|
1311
|
-
* }, null);
|
|
1312
|
-
*/
|
|
1313
|
-
export function useTimeout(callback: () => void, delay: null | number): void {
|
|
1314
|
-
useEffect(() => {
|
|
1315
|
-
if (delay === null) return;
|
|
1316
|
-
|
|
1317
|
-
const timeout = setTimeout(callback, delay);
|
|
1318
|
-
return () => {
|
|
1319
|
-
clearTimeout(timeout);
|
|
1320
|
-
};
|
|
1321
|
-
}, [callback, delay]);
|
|
1322
|
-
}
|
|
1323
|
-
`,
|
|
1324
|
-
useToggle: `import { useCallback, useState } from "react";
|
|
1325
|
-
|
|
1326
|
-
/**
|
|
1327
|
-
* Toggle a boolean value with a callback.
|
|
1328
|
-
*
|
|
1329
|
-
* @param initialValue - Initial boolean value (default: false)
|
|
1330
|
-
* @returns Tuple of [value, toggle, setValue]
|
|
1331
|
-
*
|
|
1332
|
-
* @example
|
|
1333
|
-
* const [isOpen, toggle] = useToggle(false);
|
|
1334
|
-
*
|
|
1335
|
-
* return (
|
|
1336
|
-
* <>
|
|
1337
|
-
* <button onClick={toggle}>Toggle</button>
|
|
1338
|
-
* {isOpen && <div>Content</div>}
|
|
1339
|
-
* </>
|
|
1340
|
-
* );
|
|
1341
|
-
*/
|
|
1342
|
-
export function useToggle(
|
|
1343
|
-
initialValue = false,
|
|
1344
|
-
): [boolean, () => void, (value: boolean) => void] {
|
|
1345
|
-
const [value, setValue] = useState(initialValue);
|
|
1346
|
-
|
|
1347
|
-
const toggle = useCallback(() => {
|
|
1348
|
-
setValue((prev) => !prev);
|
|
1349
|
-
}, []);
|
|
1350
|
-
|
|
1351
|
-
return [value, toggle, setValue];
|
|
1352
|
-
}
|
|
1353
|
-
`,
|
|
1354
|
-
useUnmount: `import { useEffect, useRef } from "react";
|
|
1355
|
-
|
|
1356
|
-
/**
|
|
1357
|
-
* Calls a callback on component unmount.
|
|
1358
|
-
*
|
|
1359
|
-
* @param callback - Function to call on unmount
|
|
1360
|
-
*
|
|
1361
|
-
* @example
|
|
1362
|
-
* useUnmount(() => {
|
|
1363
|
-
* console.log("Component unmounting");
|
|
1364
|
-
* // Cleanup resources
|
|
1365
|
-
* });
|
|
1366
|
-
*/
|
|
1367
|
-
export function useUnmount(callback: () => void): void {
|
|
1368
|
-
const callbackRef = useRef(callback);
|
|
1369
|
-
|
|
1370
|
-
useEffect(() => {
|
|
1371
|
-
callbackRef.current = callback;
|
|
1372
|
-
}, [callback]);
|
|
1373
|
-
|
|
1374
|
-
useEffect(() => {
|
|
1375
|
-
return () => {
|
|
1376
|
-
callbackRef.current();
|
|
1377
|
-
};
|
|
1378
|
-
}, []);
|
|
1379
|
-
}
|
|
1380
|
-
`,
|
|
1381
|
-
useWindowSize: `import { useEffect, useState } from "react";
|
|
1382
|
-
|
|
1383
|
-
interface WindowSize {
|
|
1384
|
-
height: number | undefined;
|
|
1385
|
-
width: number | undefined;
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
/**
|
|
1389
|
-
* Tracks window dimensions.
|
|
1390
|
-
*
|
|
1391
|
-
* @returns Object with width and height of the window
|
|
1392
|
-
*
|
|
1393
|
-
* @example
|
|
1394
|
-
* const { width, height } = useWindowSize();
|
|
1395
|
-
*
|
|
1396
|
-
* return (
|
|
1397
|
-
* <div>
|
|
1398
|
-
* Window size: {width}x{height}
|
|
1399
|
-
* </div>
|
|
1400
|
-
* );
|
|
1401
|
-
*/
|
|
1402
|
-
export function useWindowSize(): WindowSize {
|
|
1403
|
-
const [windowSize, setWindowSize] = useState<WindowSize>({
|
|
1404
|
-
height: undefined,
|
|
1405
|
-
width: undefined,
|
|
1406
|
-
});
|
|
1407
|
-
|
|
1408
|
-
useEffect(() => {
|
|
1409
|
-
const handleResize = () => {
|
|
1410
|
-
setWindowSize({
|
|
1411
|
-
height: window.innerHeight,
|
|
1412
|
-
width: window.innerWidth,
|
|
1413
|
-
});
|
|
1414
|
-
};
|
|
1415
|
-
|
|
1416
|
-
// Call once on mount
|
|
1417
|
-
handleResize();
|
|
1418
|
-
|
|
1419
|
-
window.addEventListener("resize", handleResize);
|
|
1420
|
-
return () => {
|
|
1421
|
-
window.removeEventListener("resize", handleResize);
|
|
1422
|
-
};
|
|
1423
|
-
}, []);
|
|
1424
|
-
|
|
1425
|
-
return windowSize;
|
|
1426
|
-
}
|
|
1427
|
-
`,
|
|
1428
|
-
};
|
|
1429
5
|
export function getHookTemplate(hookName) {
|
|
1430
6
|
const template = templates[hookName];
|
|
1431
7
|
if (!template) {
|
|
@@ -1433,4 +9,11 @@ export function getHookTemplate(hookName) {
|
|
|
1433
9
|
}
|
|
1434
10
|
return template;
|
|
1435
11
|
}
|
|
12
|
+
export function getHookTestTemplate(hookName) {
|
|
13
|
+
const template = templates[`${hookName}_test`];
|
|
14
|
+
if (!template) {
|
|
15
|
+
throw new Error(`Test template for hook "${hookName}" not found`);
|
|
16
|
+
}
|
|
17
|
+
return template;
|
|
18
|
+
}
|
|
1436
19
|
//# sourceMappingURL=get-hook-template.js.map
|