@versini/ui-hooks 5.3.2 → 6.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.
Files changed (45) hide show
  1. package/README.md +35 -9
  2. package/dist/__tests__/__mocks__/ResizeObserver.d.ts +21 -0
  3. package/dist/__tests__/useClickOutside.test.d.ts +1 -0
  4. package/dist/__tests__/useHaptic.test.d.ts +1 -0
  5. package/dist/__tests__/useHotkeys.test.d.ts +1 -0
  6. package/dist/__tests__/useInterval.test.d.ts +1 -0
  7. package/dist/__tests__/useIsMounted.test.d.ts +1 -0
  8. package/dist/__tests__/useLocalStorage.test.d.ts +1 -0
  9. package/dist/__tests__/useMergeRefs.test.d.ts +1 -0
  10. package/dist/__tests__/useResizeObserver.test.d.ts +1 -0
  11. package/dist/__tests__/useUncontrolled.test.d.ts +1 -0
  12. package/dist/__tests__/useUniqueId.test.d.ts +1 -0
  13. package/dist/__tests__/useViewportSize.test.d.ts +1 -0
  14. package/dist/__tests__/utilities.test.d.ts +1 -0
  15. package/dist/useClickOutside/useClickOutside.d.ts +17 -0
  16. package/dist/useClickOutside/useClickOutside.js +68 -0
  17. package/dist/useHaptic/useHaptic.d.ts +24 -0
  18. package/dist/useHaptic/useHaptic.js +200 -0
  19. package/dist/useHotkeys/useHotkeys.d.ts +10 -0
  20. package/dist/useHotkeys/useHotkeys.js +65 -0
  21. package/dist/useHotkeys/utilities.d.ts +19 -0
  22. package/dist/useHotkeys/utilities.js +87 -0
  23. package/dist/useInViewport/useInViewport.d.ts +10 -0
  24. package/dist/useInViewport/useInViewport.js +52 -0
  25. package/dist/useInterval/useInterval.d.ts +21 -0
  26. package/dist/useInterval/useInterval.js +75 -0
  27. package/dist/useIsMounted/useIsMounted.d.ts +13 -0
  28. package/dist/useIsMounted/useIsMounted.js +46 -0
  29. package/dist/useLocalStorage/useLocalStorage.d.ts +82 -0
  30. package/dist/useLocalStorage/useLocalStorage.js +236 -0
  31. package/dist/useMergeRefs/useMergeRefs.d.ts +20 -0
  32. package/dist/useMergeRefs/useMergeRefs.js +62 -0
  33. package/dist/useResizeObserver/useResizeObserver.d.ts +17 -0
  34. package/dist/useResizeObserver/useResizeObserver.js +94 -0
  35. package/dist/useUncontrolled/useUncontrolled.d.ts +14 -0
  36. package/dist/useUncontrolled/useUncontrolled.js +76 -0
  37. package/dist/useUniqueId/useUniqueId.d.ts +36 -0
  38. package/dist/useUniqueId/useUniqueId.js +41 -0
  39. package/dist/useViewportSize/useViewportSize.d.ts +13 -0
  40. package/dist/useViewportSize/useViewportSize.js +60 -0
  41. package/dist/useVisualViewportSize/useVisualViewportSize.d.ts +14 -0
  42. package/dist/useVisualViewportSize/useVisualViewportSize.js +70 -0
  43. package/package.json +56 -4
  44. package/dist/index.d.ts +0 -316
  45. package/dist/index.js +0 -958
package/dist/index.js DELETED
@@ -1,958 +0,0 @@
1
- /*!
2
- @versini/ui-hooks v5.3.2
3
- © 2025 gizmette.com
4
- */
5
- try {
6
- if (!window.__VERSINI_UI_HOOKS__) {
7
- window.__VERSINI_UI_HOOKS__ = {
8
- version: "5.3.2",
9
- buildTime: "12/16/2025 01:48 PM EST",
10
- homepage: "https://www.npmjs.com/package/@versini/ui-hooks",
11
- license: "MIT",
12
- };
13
- }
14
- } catch (error) {
15
- // nothing to declare officer
16
- }
17
-
18
- import { useCallback, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from "react";
19
-
20
- ;// CONCATENATED MODULE: external "react"
21
-
22
- ;// CONCATENATED MODULE: ./src/hooks/useClickOutside.tsx
23
-
24
- const DEFAULT_EVENTS = [
25
- "mousedown",
26
- "touchstart"
27
- ];
28
- /**
29
- * Custom hooks that triggers a callback when a click is detected
30
- * outside the target element.
31
- *
32
- * @param handler - Function to be called when clicked outside
33
- * @param events - Array of events to listen to
34
- * @param nodes - Array of nodes to check against
35
- * @returns Ref to be attached to the target element
36
- *
37
- * @example
38
- * const ref = useClickOutside(() => {
39
- * console.log('Clicked outside!');
40
- * });
41
- * <div ref={ref}>Click me!</div>
42
- *
43
- */ function useClickOutside(handler, events, nodes) {
44
- const ref = useRef(null);
45
- useEffect(()=>{
46
- const listener = (event)=>{
47
- /* v8 ignore next 1 */ const target = event ? event.target : undefined;
48
- if (Array.isArray(nodes)) {
49
- /* v8 ignore next 2 */ const shouldIgnore = !document.body.contains(target) && target.tagName !== "HTML";
50
- const shouldTrigger = nodes.every((node)=>!!node && !event.composedPath().includes(node));
51
- shouldTrigger && !shouldIgnore && handler();
52
- } else if (ref.current && !ref.current.contains(target)) {
53
- handler();
54
- }
55
- };
56
- (events || DEFAULT_EVENTS).forEach((fn)=>document.addEventListener(fn, listener));
57
- return ()=>{
58
- (events || DEFAULT_EVENTS).forEach((fn)=>document.removeEventListener(fn, listener));
59
- };
60
- }, [
61
- handler,
62
- nodes,
63
- events
64
- ]);
65
- return ref;
66
- }
67
-
68
- ;// CONCATENATED MODULE: ./src/hooks/useHaptic.ts
69
-
70
- const HAPTIC_DURATION_MS = 50;
71
- const HAPTIC_INTERVAL_MS = 120;
72
- /**
73
- * Singleton state for the haptic element shared across all hook instances. This
74
- * ensures only one DOM element is created regardless of how many components use
75
- * the hook.
76
- */ let sharedLabelElement = null;
77
- let refCount = 0;
78
- /**
79
- * Creates the shared haptic element if it doesn't exist. This function is
80
- * idempotent - calling it multiple times is safe. Checks DOM directly to handle
81
- * React Strict Mode double-mounting.
82
- */ const ensureHapticElement = ()=>{
83
- /* c8 ignore next 3 */ if (typeof window === "undefined") {
84
- return;
85
- }
86
- // First check: do we have valid references that exist in the DOM?
87
- if (sharedLabelElement && document.body.contains(sharedLabelElement)) {
88
- return;
89
- }
90
- /**
91
- * Second check: does an element already exist in the DOM from a previous
92
- * mount?
93
- */ const existingLabel = document.querySelector('label[data-haptic-singleton="true"]');
94
- if (existingLabel) {
95
- sharedLabelElement = existingLabel;
96
- return;
97
- }
98
- // Clear stale references.
99
- sharedLabelElement = null;
100
- // Create new elements.
101
- const input = document.createElement("input");
102
- input.type = "checkbox";
103
- input.setAttribute("switch", "");
104
- input.style.display = "none";
105
- input.setAttribute("aria-hidden", "true");
106
- input.dataset.hapticSingleton = "true";
107
- const label = document.createElement("label");
108
- label.style.display = "none";
109
- label.setAttribute("aria-hidden", "true");
110
- label.dataset.hapticSingleton = "true";
111
- label.appendChild(input);
112
- document.body.appendChild(label);
113
- sharedLabelElement = label;
114
- };
115
- /**
116
- * Removes the shared haptic element from the DOM and clears references. Only
117
- * called when the last component using the hook unmounts.
118
- */ const cleanupHapticElement = ()=>{
119
- if (sharedLabelElement && document.body && document.body.contains(sharedLabelElement)) {
120
- document.body.removeChild(sharedLabelElement);
121
- }
122
- sharedLabelElement = null;
123
- };
124
- /**
125
- * Custom hook providing imperative haptic feedback for mobile devices. Uses
126
- * navigator.vibrate when available, falls back to iOS switch element trick for
127
- * Safari on iOS devices that don't support the Vibration API.
128
- *
129
- * This hook uses a singleton pattern - only one haptic element is created in
130
- * the DOM regardless of how many components use this hook. The element is
131
- * automatically cleaned up when the last component unmounts.
132
- *
133
- * @example
134
- * ```tsx
135
- * const { haptic } = useHaptic();
136
- *
137
- * // Trigger a single haptic pulse
138
- * haptic(1);
139
- *
140
- * // Trigger two rapid haptic pulses
141
- * haptic(2);
142
- * ```
143
- *
144
- */ const useHaptic = ()=>{
145
- const timeoutsRef = useRef(new Set());
146
- useEffect(()=>{
147
- // Increment reference count and create element if needed.
148
- refCount++;
149
- try {
150
- ensureHapticElement();
151
- } catch (error) {
152
- /**
153
- * If element creation fails, we need to decrement refCount immediately since
154
- * the cleanup function won't be registered. This prevents refCount from
155
- * getting out of sync.
156
- */ refCount--;
157
- throw error;
158
- }
159
- return ()=>{
160
- /**
161
- * Cleanup function: clear all pending timeouts to prevent haptics from
162
- * firing after unmount, and decrement the reference count. Only remove the
163
- * DOM element when the last component unmounts.
164
- */ for (const timeoutId of timeoutsRef.current){
165
- clearTimeout(timeoutId);
166
- }
167
- timeoutsRef.current.clear();
168
- refCount--;
169
- if (refCount === 0) {
170
- cleanupHapticElement();
171
- }
172
- };
173
- }, []);
174
- /**
175
- * Triggers a single haptic pulse using either the Vibration API or the switch
176
- * element trick.
177
- */ const triggerSingleHaptic = useCallback(()=>{
178
- try {
179
- if (navigator?.vibrate) {
180
- navigator.vibrate(HAPTIC_DURATION_MS);
181
- return;
182
- }
183
- sharedLabelElement?.click();
184
- } catch {
185
- // Silently fail if haptics are not supported.
186
- }
187
- }, []);
188
- /**
189
- * Triggers haptic feedback with the specified number of pulses.
190
- *
191
- * @param count - Number of haptic pulses to trigger (default: 1). For count > 1,
192
- * pulses are triggered in rapid succession with a 120ms interval between each
193
- * pulse.
194
- *
195
- * @example
196
- * ```tsx
197
- * const { haptic } = useHaptic();
198
- *
199
- * // Single haptic
200
- * haptic(1);
201
- *
202
- * // Two rapid haptics
203
- * haptic(2);
204
- *
205
- * // Three rapid haptics
206
- * haptic(3);
207
- * ```
208
- *
209
- */ const haptic = useCallback((count = 1)=>{
210
- /* c8 ignore next 3 */ if (typeof window === "undefined") {
211
- return;
212
- }
213
- if (count < 1) {
214
- return;
215
- }
216
- if (navigator?.vibrate && count > 1) {
217
- /**
218
- * For multi-haptic patterns with navigator.vibrate, create an array pattern
219
- * like [50, 120, 50, 120, 50] for better timing control.
220
- */ const pattern = [];
221
- for(let i = 0; i < count; i++){
222
- pattern.push(HAPTIC_DURATION_MS);
223
- if (i < count - 1) {
224
- pattern.push(HAPTIC_INTERVAL_MS - HAPTIC_DURATION_MS);
225
- }
226
- }
227
- navigator.vibrate(pattern);
228
- return;
229
- }
230
- // For switch element or single vibration, trigger sequentially.
231
- for(let i = 0; i < count; i++){
232
- const timeoutId = setTimeout(()=>{
233
- triggerSingleHaptic();
234
- timeoutsRef.current.delete(timeoutId);
235
- }, i * HAPTIC_INTERVAL_MS);
236
- timeoutsRef.current.add(timeoutId);
237
- }
238
- }, [
239
- triggerSingleHaptic
240
- ]);
241
- return {
242
- haptic
243
- };
244
- };
245
-
246
- ;// CONCATENATED MODULE: ./src/hooks/utilities.ts
247
- function parseHotkey(hotkey) {
248
- const keys = hotkey.toLowerCase().split("+").map((part)=>part.trim());
249
- const modifiers = {
250
- alt: keys.includes("alt"),
251
- ctrl: keys.includes("ctrl"),
252
- meta: keys.includes("meta"),
253
- mod: keys.includes("mod"),
254
- shift: keys.includes("shift")
255
- };
256
- const reservedKeys = [
257
- "alt",
258
- "ctrl",
259
- "meta",
260
- "shift",
261
- "mod"
262
- ];
263
- const freeKey = keys.find((key)=>!reservedKeys.includes(key));
264
- return {
265
- ...modifiers,
266
- key: freeKey
267
- };
268
- }
269
- function isExactHotkey(hotkey, event) {
270
- const { alt, ctrl, meta, mod, shift, key } = hotkey;
271
- const { altKey, ctrlKey, metaKey, shiftKey, key: pressedKey } = event;
272
- if (alt !== altKey) {
273
- return false;
274
- }
275
- if (mod) {
276
- if (!ctrlKey && !metaKey) {
277
- return false;
278
- }
279
- } else {
280
- if (ctrl !== ctrlKey) {
281
- return false;
282
- }
283
- if (meta !== metaKey) {
284
- return false;
285
- }
286
- }
287
- if (shift !== shiftKey) {
288
- return false;
289
- }
290
- if (key && (pressedKey.toLowerCase() === key.toLowerCase() || event.code.replace("Key", "").toLowerCase() === key.toLowerCase())) {
291
- return true;
292
- }
293
- return false;
294
- }
295
- function getHotkeyMatcher(hotkey) {
296
- return (event)=>isExactHotkey(parseHotkey(hotkey), event);
297
- }
298
- function getHotkeyHandler(hotkeys) {
299
- return (event)=>{
300
- const _event = "nativeEvent" in event ? event.nativeEvent : event;
301
- hotkeys.forEach(([hotkey, handler, options = {
302
- preventDefault: true
303
- }])=>{
304
- if (getHotkeyMatcher(hotkey)(_event)) {
305
- if (options.preventDefault) {
306
- event.preventDefault();
307
- }
308
- handler(_event);
309
- }
310
- });
311
- };
312
- }
313
-
314
- ;// CONCATENATED MODULE: ./src/hooks/useHotkeys.ts
315
-
316
-
317
-
318
- function shouldFireEvent(event, tagsToIgnore, triggerOnContentEditable = false) {
319
- if (event.target instanceof HTMLElement) {
320
- if (triggerOnContentEditable) {
321
- return !tagsToIgnore.includes(event.target.tagName);
322
- }
323
- return !event.target.isContentEditable && !tagsToIgnore.includes(event.target.tagName);
324
- }
325
- return true;
326
- }
327
- function useHotkeys(hotkeys, tagsToIgnore = [
328
- "INPUT",
329
- "TEXTAREA",
330
- "SELECT"
331
- ], triggerOnContentEditable = false) {
332
- useEffect(()=>{
333
- const keydownListener = (event)=>{
334
- hotkeys.forEach(([hotkey, handler, options = {
335
- preventDefault: true
336
- }])=>{
337
- if (getHotkeyMatcher(hotkey)(event) && shouldFireEvent(event, tagsToIgnore, triggerOnContentEditable)) {
338
- if (options.preventDefault) {
339
- event.preventDefault();
340
- }
341
- handler(event);
342
- }
343
- });
344
- };
345
- document.documentElement.addEventListener("keydown", keydownListener);
346
- return ()=>document.documentElement.removeEventListener("keydown", keydownListener);
347
- }, [
348
- hotkeys,
349
- tagsToIgnore,
350
- triggerOnContentEditable
351
- ]);
352
- }
353
-
354
- ;// CONCATENATED MODULE: ./src/hooks/useInterval.ts
355
-
356
- /**
357
- * Custom hook to call a function within a given interval.
358
- *
359
- * @param fn Callback function to be executed at each interval
360
- * @param interval Interval time in milliseconds
361
- * @returns An object containing start, stop, and active state
362
- *
363
- * @example
364
- * const { start, stop, active } = useInterval(() => {
365
- * console.log("Interval executed");
366
- * }, 1000);
367
- * start(); // To start the interval
368
- * stop(); // To stop the interval
369
- * console.log(active); // To check if the interval is active
370
- *
371
- */ function useInterval(fn, interval) {
372
- const [active, setActive] = useState(false);
373
- const intervalRef = useRef(null);
374
- const fnRef = useRef(null);
375
- const start = useCallback(()=>{
376
- setActive((old)=>{
377
- if (!old && (!intervalRef.current || intervalRef.current === -1)) {
378
- intervalRef.current = window.setInterval(fnRef.current, interval);
379
- }
380
- return true;
381
- });
382
- }, [
383
- interval
384
- ]);
385
- const stop = useCallback(()=>{
386
- setActive(false);
387
- window.clearInterval(intervalRef.current || -1);
388
- intervalRef.current = -1;
389
- }, []);
390
- useEffect(()=>{
391
- fnRef.current = fn;
392
- active && start();
393
- return stop;
394
- }, [
395
- fn,
396
- active,
397
- start,
398
- stop
399
- ]);
400
- return {
401
- start,
402
- stop,
403
- active
404
- };
405
- }
406
-
407
- ;// CONCATENATED MODULE: ./src/hooks/useInViewport.ts
408
-
409
- /**
410
- * Hook that checks if an element is visible in the viewport.
411
- * @returns
412
- * ref: React ref object to attach to the element you want to monitor.
413
- * inViewport: Boolean indicating if the element is in the viewport.
414
- */ /* v8 ignore next 24 */ function useInViewport() {
415
- const observer = useRef(null);
416
- const [inViewport, setInViewport] = useState(false);
417
- const ref = useCallback((node)=>{
418
- if (typeof IntersectionObserver !== "undefined") {
419
- if (node && !observer.current) {
420
- observer.current = new IntersectionObserver((entries)=>setInViewport(entries.some((entry)=>entry.isIntersecting)));
421
- } else {
422
- observer.current?.disconnect();
423
- }
424
- if (node) {
425
- observer.current?.observe(node);
426
- } else {
427
- setInViewport(false);
428
- }
429
- }
430
- }, []);
431
- return {
432
- ref,
433
- inViewport
434
- };
435
- }
436
-
437
- ;// CONCATENATED MODULE: ./src/hooks/useIsMounted.ts
438
-
439
- /**
440
- * Custom hook that returns a function indicating whether the component
441
- * is mounted or not.
442
- *
443
- * @returns A function that returns a boolean value indicating whether
444
- * the component is mounted.
445
- *
446
- * @example
447
- * const isMounted = useIsMounted();
448
- * console.log(isMounted()); // true
449
- *
450
- */ function useIsMounted() {
451
- const isMounted = useRef(false);
452
- useEffect(()=>{
453
- isMounted.current = true;
454
- return ()=>{
455
- isMounted.current = false;
456
- };
457
- }, []);
458
- return useCallback(()=>isMounted.current, []);
459
- }
460
-
461
- ;// CONCATENATED MODULE: ./src/hooks/useLocalStorage.ts
462
-
463
- /**
464
- * Dispatches a custom storage event to notify other parts of the application
465
- * about localStorage changes. This is necessary because the native storage
466
- * event only fires in other tabs/windows, not in the current one. By manually
467
- * dispatching the event, we ensure that all components using the same key stay
468
- * synchronized.
469
- *
470
- * @param key - The localStorage key that was modified
471
- * @param newValue - The new value (stringified) or null if the item was removed
472
- *
473
- */ function dispatchStorageEvent(key, newValue) {
474
- window.dispatchEvent(new StorageEvent("storage", {
475
- key,
476
- newValue
477
- }));
478
- }
479
- /**
480
- * Sets an item in localStorage and dispatches a storage event. Handles both
481
- * direct values and function updaters (similar to React's setState). Values are
482
- * automatically serialized to JSON before storage.
483
- *
484
- * @param key - The localStorage key to set
485
- * @param value - The value to store, or a function that returns the value to store
486
- *
487
- */ const setLocalStorageItem = (key, value)=>{
488
- /**
489
- * If value is a function, call it to get the actual value (supports functional
490
- * updates).
491
- */ const stringifiedValue = JSON.stringify(typeof value === "function" ? value() : value);
492
- window.localStorage.setItem(key, stringifiedValue);
493
- // Dispatch event to notify other components in the same window/tab.
494
- dispatchStorageEvent(key, stringifiedValue);
495
- };
496
- /**
497
- * Removes an item from localStorage and dispatches a storage event with null
498
- * value. This ensures all components using this key are notified of the
499
- * removal.
500
- *
501
- * @param key - The localStorage key to remove
502
- *
503
- */ const removeLocalStorageItem = (key)=>{
504
- window.localStorage.removeItem(key);
505
- // Dispatch event with null to signal removal.
506
- dispatchStorageEvent(key, null);
507
- };
508
- /**
509
- * Retrieves an item from localStorage. Returns the raw stringified value or
510
- * null if the key doesn't exist.
511
- *
512
- * @param key - The localStorage key to retrieve
513
- * @returns The stored string value or null if not found
514
- *
515
- */ const getLocalStorageItem = (key)=>{
516
- return window.localStorage.getItem(key);
517
- };
518
- /**
519
- * Creates a subscription to localStorage changes for use with React's
520
- * useSyncExternalStore. This function is called by React to set up and tear
521
- * down event listeners. It listens to both native storage events (from other
522
- * tabs) and custom dispatched events (from this tab).
523
- *
524
- * @param callback - The callback function to invoke when storage changes occur
525
- * @returns A cleanup function that removes the event listener
526
- *
527
- */ const useLocalStorageSubscribe = (callback)=>{
528
- window.addEventListener("storage", callback);
529
- // Return cleanup function for React to call on unmount.
530
- return ()=>window.removeEventListener("storage", callback);
531
- };
532
- /**
533
- * A React hook for managing state synchronized with localStorage. Uses React
534
- * 19's useSyncExternalStore for optimal concurrent rendering support and
535
- * automatic synchronization across components. Changes are automatically
536
- * persisted to localStorage and synchronized across all components using the
537
- * same key, including across browser tabs.
538
- *
539
- * Features:
540
- * - Automatic serialization/deserialization with JSON
541
- * - Type-safe with TypeScript generics
542
- * - Supports functional updates (similar to setState)
543
- * - Synchronized across components and browser tabs
544
- * - Handles edge cases (null, undefined, errors)
545
- * - Compatible with React 19+ concurrent features
546
- *
547
- * @template T - The type of the value stored in localStorage
548
- * @param {StorageProperties<T>} config - Configuration object with key and optional initialValue
549
- * @returns {[T | null, (value: T | ((current: T) => T)) => void, () => void, () => void]} A tuple containing:
550
- * - [0] current value from localStorage (or null if not set)
551
- * - [1] setValue function to update the stored value (supports direct value or function updater)
552
- * - [2] resetValue function to restore the initialValue
553
- * - [3] removeValue function to remove the value from localStorage
554
- *
555
- * @example
556
- * ```js
557
- * // Basic usage with a string value
558
- * import { useLocalStorage } from '@versini/ui-hooks';
559
- * const [model, setModel, resetModel, removeModel] = useLocalStorage({
560
- * key: 'gpt-model',
561
- * initialValue: 'gpt-3',
562
- * });
563
- *
564
- * // Direct update
565
- * setModel('gpt-4'); // Stores "gpt-4"
566
- *
567
- * // Functional update (receives current value)
568
- * setModel((current) => (current === 'gpt-3' ? 'gpt-4' : 'gpt-3'));
569
- *
570
- * // Reset to initial value
571
- * resetModel(); // Restores "gpt-3"
572
- *
573
- * // Remove from localStorage
574
- * removeModel(); // Sets value to null and removes from storage
575
- * ```
576
- *
577
- * @example
578
- * ```js
579
- * // Usage with complex objects
580
- * interface UserPreferences {
581
- * theme: 'light' | 'dark';
582
- * fontSize: number;
583
- * }
584
- *
585
- * const [prefs, setPrefs] = useLocalStorage<UserPreferences>({
586
- * key: 'user-preferences',
587
- * initialValue: { theme: 'light', fontSize: 14 }
588
- * });
589
- *
590
- * // Update specific property
591
- * setPrefs(current => ({ ...current, theme: 'dark' }));
592
- * ```
593
- *
594
- */ function useLocalStorage({ key, initialValue }) {
595
- /**
596
- * Snapshot function for useSyncExternalStore - returns current localStorage
597
- * value.
598
- */ const getSnapshot = ()=>getLocalStorageItem(key);
599
- /**
600
- * Use React's useSyncExternalStore to subscribe to localStorage changes. This
601
- * ensures proper integration with React 19+ concurrent rendering and provides
602
- * automatic re-rendering when the stored value changes (either from this
603
- * component, other components, or other browser tabs).
604
- */ const store = useSyncExternalStore(useLocalStorageSubscribe, getSnapshot);
605
- /**
606
- * Updates the stored value in localStorage. Accepts either a direct value or a
607
- * function that receives the current value. Setting to null or undefined will
608
- * remove the item from localStorage.
609
- */ const setValue = useCallback((v)=>{
610
- try {
611
- // Support both direct values and functional updates.
612
- const nextState = typeof v === "function" ? v(JSON.parse(store)) : v;
613
- // Remove from storage if value is null or undefined.
614
- if (nextState === undefined || nextState === null) {
615
- removeLocalStorageItem(key);
616
- } else {
617
- setLocalStorageItem(key, nextState);
618
- }
619
- /* v8 ignore next 4 */ } catch (e) {
620
- // Log parsing or storage errors without breaking the application.
621
- console.warn(e);
622
- }
623
- }, [
624
- key,
625
- store
626
- ]);
627
- /**
628
- * Resets the stored value back to the initialValue provided in the
629
- * configuration. If no initialValue was provided, this will remove the item
630
- * from localStorage.
631
- */ const resetValue = useCallback(()=>{
632
- setValue(initialValue);
633
- }, [
634
- initialValue,
635
- setValue
636
- ]);
637
- /**
638
- * Removes the value from localStorage entirely. After calling this, the hook
639
- * will return null until a new value is set.
640
- */ const removeValue = useCallback(()=>{
641
- setValue(null);
642
- }, [
643
- setValue
644
- ]);
645
- /**
646
- * Initialize localStorage with the initialValue on first mount if the key
647
- * doesn't exist. This effect only runs once when the component mounts and
648
- * ensures that the initialValue is persisted to localStorage if no value is
649
- * currently stored.
650
- */ useEffect(()=>{
651
- try {
652
- // Only set initialValue if key doesn't exist and initialValue is defined.
653
- if (getLocalStorageItem(key) === null && typeof initialValue !== "undefined") {
654
- setLocalStorageItem(key, initialValue);
655
- }
656
- /* v8 ignore next 4 */ } catch (e) {
657
- // Log initialization errors without breaking the application.
658
- console.warn(e);
659
- }
660
- }, [
661
- key,
662
- initialValue
663
- ]);
664
- /**
665
- * Return tuple: [currentValue, setValue, resetValue, removeValue] Parse the
666
- * stored JSON string back to its original type, or return null if empty.
667
- */ return [
668
- store ? JSON.parse(store) : null,
669
- setValue,
670
- resetValue,
671
- removeValue
672
- ];
673
- }
674
-
675
- ;// CONCATENATED MODULE: ./src/hooks/useMergeRefs.ts
676
- /**
677
- * React utility to merge refs.
678
- *
679
- * When developing low level UI components, it is common to have to use a local
680
- * ref but also support an external one using React.forwardRef. Natively, React
681
- * does not offer a way to set two refs inside the ref property.
682
- *
683
- * @param Array of refs (object, function, etc.)
684
- *
685
- * @example
686
- *
687
- * const Example = React.forwardRef(function Example(props, ref) {
688
- * const localRef = React.useRef();
689
- * const mergedRefs = useMergeRefs([localRef, ref]);
690
- *
691
- * return <div ref={mergedRefs} />;
692
- * });
693
- *
694
- */
695
- function useMergeRefs(refs) {
696
- // biome-ignore lint/correctness/useExhaustiveDependencies: refs array is used as dependency but spread for proper comparison
697
- return useMemo(()=>{
698
- if (refs.every((ref)=>ref == null)) {
699
- return ()=>{};
700
- }
701
- return (value)=>{
702
- refs.forEach((ref)=>{
703
- if (typeof ref === "function") {
704
- ref(value);
705
- } else if (ref != null) {
706
- ref.current = value;
707
- }
708
- });
709
- };
710
- }, [
711
- ...refs
712
- ]);
713
- }
714
-
715
- ;// CONCATENATED MODULE: ./src/hooks/useResizeObserver.ts
716
-
717
-
718
- const defaultState = {
719
- x: 0,
720
- y: 0,
721
- width: 0,
722
- height: 0,
723
- top: 0,
724
- left: 0,
725
- bottom: 0,
726
- right: 0
727
- };
728
- /**
729
- * A custom hook that uses the ResizeObserver API to track the size changes of a DOM element.
730
- *
731
- * @template T - The type of the DOM element being observed.
732
- * @param {ResizeObserverOptions} [options] - The options to configure the ResizeObserver.
733
- * @returns {[React.RefObject<T>, ObserverRect]} - A tuple containing the ref object and
734
- * the observed rectangle.
735
- * @example
736
- *
737
- * const [rightElementRef, rect] = useResizeObserver<HTMLDivElement>();
738
- * <div ref={componentRef}>
739
- * Observed: <code>{JSON.stringify(rect)}</code>
740
- * </div>
741
- */ function useResizeObserver(options) {
742
- const isMounted = useIsMounted();
743
- const frameID = useRef(0);
744
- const ref = useRef(null);
745
- const [rect, setRect] = useState(defaultState);
746
- const observer = useMemo(()=>{
747
- /* c8 ignore next 3 */ if (typeof ResizeObserver === "undefined") {
748
- return null;
749
- }
750
- return new ResizeObserver((entries)=>{
751
- const entry = entries[0];
752
- if (entry) {
753
- cancelAnimationFrame(frameID.current);
754
- frameID.current = requestAnimationFrame(()=>{
755
- if (ref.current && isMounted()) {
756
- setRect(entry.contentRect);
757
- }
758
- });
759
- }
760
- });
761
- }, [
762
- isMounted
763
- ]);
764
- useEffect(()=>{
765
- /* c8 ignore next 3 */ if (ref.current) {
766
- observer?.observe(ref.current, options);
767
- }
768
- return ()=>{
769
- observer?.disconnect();
770
- /* c8 ignore next 3 */ if (frameID.current) {
771
- cancelAnimationFrame(frameID.current);
772
- }
773
- };
774
- }, [
775
- observer,
776
- options
777
- ]);
778
- return [
779
- ref,
780
- rect
781
- ];
782
- }
783
-
784
- ;// CONCATENATED MODULE: ./src/hooks/useUncontrolled.ts
785
-
786
- function useUncontrolled({ value, defaultValue, finalValue, onChange = ()=>{}, initialControlledDelay = 0 }) {
787
- const [initialDelayDone, setInitialDelayDone] = useState(false);
788
- const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue !== undefined ? defaultValue : finalValue);
789
- const handleUncontrolledChange = (val)=>{
790
- setUncontrolledValue(val);
791
- onChange?.(val);
792
- };
793
- useEffect(()=>{
794
- (async ()=>{
795
- /**
796
- * If initialControlledDelay is provided, wait for the delay.
797
- */ if (value !== undefined) {
798
- /* c8 ignore start */ if (!initialDelayDone && initialControlledDelay > 0) {
799
- await new Promise((resolve)=>setTimeout(resolve, initialControlledDelay));
800
- setInitialDelayDone(true);
801
- }
802
- /* c8 ignore end */ }
803
- })();
804
- }, [
805
- value,
806
- initialControlledDelay,
807
- initialDelayDone
808
- ]);
809
- /**
810
- * If value is provided, return the controlled value.
811
- * If there is a delay, we need to wait for the delay: we need to first send
812
- * back a value of an empty string, then after the delay
813
- * we can send the actual value.
814
- */ if (value !== undefined) {
815
- if (!initialDelayDone && initialControlledDelay > 0) {
816
- return [
817
- "",
818
- onChange,
819
- true
820
- ];
821
- } else {
822
- return [
823
- value,
824
- onChange,
825
- true
826
- ];
827
- }
828
- }
829
- /**
830
- * If value is not provided, return the uncontrolled value.
831
- */ return [
832
- uncontrolledValue,
833
- handleUncontrolledChange,
834
- false
835
- ];
836
- }
837
-
838
- ;// CONCATENATED MODULE: ./src/hooks/useUniqueId.ts
839
-
840
- function useUniqueId(options) {
841
- const generatedId = useId();
842
- if (!options) {
843
- return generatedId;
844
- }
845
- if (typeof options === "number" || typeof options === "string") {
846
- return `${options}${generatedId}`;
847
- }
848
- if (typeof options === "object") {
849
- const { id, prefix = "" } = options;
850
- if (typeof id === "number" || typeof id === "string") {
851
- return `${prefix}${id}`;
852
- }
853
- return `${prefix}${generatedId}`;
854
- }
855
- }
856
-
857
- ;// CONCATENATED MODULE: ./src/hooks/useViewportSize.tsx
858
-
859
- function useWindowEvent(type, listener) {
860
- useEffect(()=>{
861
- window.addEventListener(type, listener, {
862
- passive: true
863
- });
864
- return ()=>window.removeEventListener(type, listener);
865
- }, [
866
- type,
867
- listener
868
- ]);
869
- }
870
- /**
871
- * Custom hook that returns the current viewport size. It will update
872
- * when the window is resized or the orientation changes.
873
- *
874
- * @returns The current viewport size
875
- *
876
- * @example
877
- * const { width, height } = useViewportSize();
878
- */ function useViewportSize() {
879
- const [windowSize, setWindowSize] = useState({
880
- width: 0,
881
- height: 0
882
- });
883
- const setSize = useCallback(()=>{
884
- setWindowSize({
885
- width: window.innerWidth || 0,
886
- height: window.innerHeight || 0
887
- });
888
- }, []);
889
- useWindowEvent("resize", setSize);
890
- useWindowEvent("orientationchange", setSize);
891
- useEffect(setSize, []);
892
- return windowSize;
893
- }
894
-
895
- ;// CONCATENATED MODULE: ./src/hooks/useVisualViewportSize.tsx
896
- /* v8 ignore start */
897
- /**
898
- * Custom hook that returns the current visual viewport size. It will update
899
- * when the window is resized (zoom, virtual keyboard displayed, etc.) or
900
- * the orientation changes.
901
- *
902
- * @returns The current visual viewport size
903
- *
904
- * @example
905
- * const { width, height } = useVisualViewportSize();
906
- */ function useVisualViewportSize() {
907
- const [windowSize, setWindowSize] = useState({
908
- width: 0,
909
- height: 0
910
- });
911
- // Define setSize function once and never recreate it
912
- const setSize = useCallback(()=>{
913
- setWindowSize({
914
- width: window?.visualViewport?.width || window.innerWidth || 0,
915
- height: window?.visualViewport?.height || window.innerHeight || 0
916
- });
917
- }, []); // Empty dependency array is correct here
918
- useEffect(()=>{
919
- // Initial size setup
920
- setSize();
921
- // Set up event listeners
922
- if (window.visualViewport) {
923
- window.visualViewport.addEventListener("resize", setSize, {
924
- passive: true
925
- });
926
- window.visualViewport.addEventListener("orientationchange", setSize, {
927
- passive: true
928
- });
929
- }
930
- // Cleanup
931
- return ()=>{
932
- if (window.visualViewport) {
933
- window.visualViewport.removeEventListener("resize", setSize);
934
- window.visualViewport.removeEventListener("orientationchange", setSize);
935
- }
936
- };
937
- }, [
938
- setSize
939
- ]);
940
- return windowSize;
941
- } /* v8 ignore end */
942
-
943
- ;// CONCATENATED MODULE: ./src/hooks/index.ts
944
-
945
-
946
-
947
-
948
-
949
-
950
-
951
-
952
-
953
-
954
-
955
-
956
-
957
-
958
- export { getHotkeyHandler, shouldFireEvent, useClickOutside, useHaptic, useHotkeys, useInViewport, useInterval, useIsMounted, useLocalStorage, useMergeRefs, useResizeObserver, useUncontrolled, useUniqueId, useViewportSize, useVisualViewportSize };