@versini/ui-hooks 5.2.0 → 5.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/dist/index.d.ts +97 -7
- package/dist/index.js +308 -25
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -18,13 +18,21 @@ declare type ObserverRect = Omit<DOMRectReadOnly, "toJSON">;
|
|
|
18
18
|
|
|
19
19
|
export declare function shouldFireEvent(event: KeyboardEvent, tagsToIgnore: string[], triggerOnContentEditable?: boolean): boolean;
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Configuration properties for the useLocalStorage hook.
|
|
23
|
+
*
|
|
24
|
+
* @template T - The type of the value stored in localStorage
|
|
25
|
+
*
|
|
26
|
+
*/
|
|
21
27
|
export declare interface StorageProperties<T> {
|
|
22
28
|
/**
|
|
23
|
-
*
|
|
29
|
+
* The localStorage key under which the value will be stored. Must be unique
|
|
30
|
+
* within your application to avoid conflicts.
|
|
24
31
|
*/
|
|
25
32
|
key: string;
|
|
26
33
|
/**
|
|
27
|
-
*
|
|
34
|
+
* Optional default value that will be set in localStorage if no value exists.
|
|
35
|
+
* This value will be used on initial mount and when resetValue() is called.
|
|
28
36
|
*/
|
|
29
37
|
initialValue?: T;
|
|
30
38
|
}
|
|
@@ -47,6 +55,39 @@ export declare interface StorageProperties<T> {
|
|
|
47
55
|
*/
|
|
48
56
|
export declare function useClickOutside<T extends HTMLElement = any>(handler: () => void, events?: string[] | null, nodes?: (HTMLElement | null)[]): RefObject<T | null>;
|
|
49
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Custom hook for trapping focus within a container element. Implements W3C
|
|
60
|
+
* WAI-ARIA dialog focus management patterns:
|
|
61
|
+
* - Tab/Shift+Tab cycles through focusable elements within the container
|
|
62
|
+
* - Focus is trapped and cannot escape to elements outside
|
|
63
|
+
* - Initial focus is set based on the initialFocus option
|
|
64
|
+
*
|
|
65
|
+
* @see https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
|
|
66
|
+
*
|
|
67
|
+
*/
|
|
68
|
+
export declare function useFocusTrap({ enabled, initialFocus, }: UseFocusTrapOptions): UseFocusTrapReturn;
|
|
69
|
+
|
|
70
|
+
declare interface UseFocusTrapOptions {
|
|
71
|
+
/**
|
|
72
|
+
* Whether the focus trap is active.
|
|
73
|
+
*/
|
|
74
|
+
enabled: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Which element to initially focus when the trap activates. Can be a number
|
|
77
|
+
* (tabbable index, 0 = first), a ref to an element, or -1 to disable initial
|
|
78
|
+
* focus.
|
|
79
|
+
* @default 0
|
|
80
|
+
*/
|
|
81
|
+
initialFocus?: number | React.RefObject<HTMLElement | null>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
declare interface UseFocusTrapReturn {
|
|
85
|
+
/**
|
|
86
|
+
* Ref to attach to the container element that should trap focus.
|
|
87
|
+
*/
|
|
88
|
+
containerRef: React.RefObject<HTMLDivElement | null>;
|
|
89
|
+
}
|
|
90
|
+
|
|
50
91
|
/**
|
|
51
92
|
* Custom hook providing imperative haptic feedback for mobile devices. Uses
|
|
52
93
|
* navigator.vibrate when available, falls back to iOS switch element trick for
|
|
@@ -122,18 +163,67 @@ export declare function useInViewport<T extends HTMLElement = any>(): {
|
|
|
122
163
|
export declare function useIsMounted(): () => boolean;
|
|
123
164
|
|
|
124
165
|
/**
|
|
166
|
+
* A React hook for managing state synchronized with localStorage. Uses React
|
|
167
|
+
* 19's useSyncExternalStore for optimal concurrent rendering support and
|
|
168
|
+
* automatic synchronization across components. Changes are automatically
|
|
169
|
+
* persisted to localStorage and synchronized across all components using the
|
|
170
|
+
* same key, including across browser tabs.
|
|
171
|
+
*
|
|
172
|
+
* Features:
|
|
173
|
+
* - Automatic serialization/deserialization with JSON
|
|
174
|
+
* - Type-safe with TypeScript generics
|
|
175
|
+
* - Supports functional updates (similar to setState)
|
|
176
|
+
* - Synchronized across components and browser tabs
|
|
177
|
+
* - Handles edge cases (null, undefined, errors)
|
|
178
|
+
* - Compatible with React 19+ concurrent features
|
|
179
|
+
*
|
|
180
|
+
* @template T - The type of the value stored in localStorage
|
|
181
|
+
* @param {StorageProperties<T>} config - Configuration object with key and optional initialValue
|
|
182
|
+
* @returns {[T | null, (value: T | ((current: T) => T)) => void, () => void, () => void]} A tuple containing:
|
|
183
|
+
* - [0] current value from localStorage (or null if not set)
|
|
184
|
+
* - [1] setValue function to update the stored value (supports direct value or function updater)
|
|
185
|
+
* - [2] resetValue function to restore the initialValue
|
|
186
|
+
* - [3] removeValue function to remove the value from localStorage
|
|
125
187
|
*
|
|
126
188
|
* @example
|
|
189
|
+
* ```js
|
|
190
|
+
* // Basic usage with a string value
|
|
127
191
|
* import { useLocalStorage } from '@versini/ui-hooks';
|
|
128
|
-
* const [
|
|
192
|
+
* const [model, setModel, resetModel, removeModel] = useLocalStorage({
|
|
129
193
|
* key: 'gpt-model',
|
|
130
194
|
* initialValue: 'gpt-3',
|
|
131
195
|
* });
|
|
132
196
|
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
197
|
+
* // Direct update
|
|
198
|
+
* setModel('gpt-4'); // Stores "gpt-4"
|
|
199
|
+
*
|
|
200
|
+
* // Functional update (receives current value)
|
|
201
|
+
* setModel((current) => (current === 'gpt-3' ? 'gpt-4' : 'gpt-3'));
|
|
202
|
+
*
|
|
203
|
+
* // Reset to initial value
|
|
204
|
+
* resetModel(); // Restores "gpt-3"
|
|
205
|
+
*
|
|
206
|
+
* // Remove from localStorage
|
|
207
|
+
* removeModel(); // Sets value to null and removes from storage
|
|
208
|
+
* ```
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* ```js
|
|
212
|
+
* // Usage with complex objects
|
|
213
|
+
* interface UserPreferences {
|
|
214
|
+
* theme: 'light' | 'dark';
|
|
215
|
+
* fontSize: number;
|
|
216
|
+
* }
|
|
217
|
+
*
|
|
218
|
+
* const [prefs, setPrefs] = useLocalStorage<UserPreferences>({
|
|
219
|
+
* key: 'user-preferences',
|
|
220
|
+
* initialValue: { theme: 'light', fontSize: 14 }
|
|
221
|
+
* });
|
|
222
|
+
*
|
|
223
|
+
* // Update specific property
|
|
224
|
+
* setPrefs(current => ({ ...current, theme: 'dark' }));
|
|
225
|
+
* ```
|
|
226
|
+
*
|
|
137
227
|
*/
|
|
138
228
|
export declare function useLocalStorage<T>({ key, initialValue, }: StorageProperties<T>): any[];
|
|
139
229
|
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
@versini/ui-hooks v5.
|
|
2
|
+
@versini/ui-hooks v5.3.0
|
|
3
3
|
© 2025 gizmette.com
|
|
4
4
|
*/
|
|
5
5
|
try {
|
|
6
6
|
if (!window.__VERSINI_UI_HOOKS__) {
|
|
7
7
|
window.__VERSINI_UI_HOOKS__ = {
|
|
8
|
-
version: "5.
|
|
9
|
-
buildTime: "
|
|
10
|
-
homepage: "https://
|
|
8
|
+
version: "5.3.0",
|
|
9
|
+
buildTime: "12/14/2025 08:04 PM EST",
|
|
10
|
+
homepage: "https://www.npmjs.com/package/@versini/ui-hooks",
|
|
11
11
|
license: "MIT",
|
|
12
12
|
};
|
|
13
13
|
}
|
|
@@ -65,6 +65,160 @@ const DEFAULT_EVENTS = [
|
|
|
65
65
|
return ref;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
;// CONCATENATED MODULE: ./src/hooks/useFocusTrap.ts
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Selector for all focusable elements within a container. Based on W3C WAI-ARIA
|
|
72
|
+
* practices for dialog focus management.
|
|
73
|
+
*/ const FOCUSABLE_SELECTOR = [
|
|
74
|
+
'a[href]:not([disabled]):not([tabindex="-1"])',
|
|
75
|
+
'button:not([disabled]):not([tabindex="-1"])',
|
|
76
|
+
'textarea:not([disabled]):not([tabindex="-1"])',
|
|
77
|
+
'input:not([disabled]):not([tabindex="-1"])',
|
|
78
|
+
'select:not([disabled]):not([tabindex="-1"])',
|
|
79
|
+
'[tabindex]:not([tabindex="-1"]):not([disabled])',
|
|
80
|
+
'audio[controls]:not([tabindex="-1"])',
|
|
81
|
+
'video[controls]:not([tabindex="-1"])',
|
|
82
|
+
'details:not([tabindex="-1"])'
|
|
83
|
+
].join(", ");
|
|
84
|
+
/**
|
|
85
|
+
* Custom hook for trapping focus within a container element. Implements W3C
|
|
86
|
+
* WAI-ARIA dialog focus management patterns:
|
|
87
|
+
* - Tab/Shift+Tab cycles through focusable elements within the container
|
|
88
|
+
* - Focus is trapped and cannot escape to elements outside
|
|
89
|
+
* - Initial focus is set based on the initialFocus option
|
|
90
|
+
*
|
|
91
|
+
* @see https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
|
|
92
|
+
*
|
|
93
|
+
*/ function useFocusTrap({ enabled, initialFocus = 0 }) {
|
|
94
|
+
const containerRef = useRef(null);
|
|
95
|
+
const previouslyFocusedRef = useRef(null);
|
|
96
|
+
/**
|
|
97
|
+
* Get all focusable elements within the container.
|
|
98
|
+
*/ const getFocusableElements = useCallback(()=>{
|
|
99
|
+
/* c8 ignore next 3 - defensive check, containerRef is always set when enabled */ if (!containerRef.current) {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
const elements = containerRef.current.querySelectorAll(FOCUSABLE_SELECTOR);
|
|
103
|
+
return Array.from(elements).filter((el)=>el.offsetParent !== null);
|
|
104
|
+
}, []);
|
|
105
|
+
/**
|
|
106
|
+
* Focus a specific element by index, or the element referenced by a ref.
|
|
107
|
+
*/ const focusElement = useCallback((target)=>{
|
|
108
|
+
if (typeof target === "number") {
|
|
109
|
+
if (target === -1) {
|
|
110
|
+
// -1 means don't focus anything.
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const focusableElements = getFocusableElements();
|
|
114
|
+
if (focusableElements.length > 0) {
|
|
115
|
+
const index = Math.min(target, focusableElements.length - 1);
|
|
116
|
+
focusableElements[index]?.focus();
|
|
117
|
+
}
|
|
118
|
+
} else if (target?.current) {
|
|
119
|
+
target.current.focus();
|
|
120
|
+
}
|
|
121
|
+
}, [
|
|
122
|
+
getFocusableElements
|
|
123
|
+
]);
|
|
124
|
+
/**
|
|
125
|
+
* Handle keyboard events for focus trapping. Tab cycles forward, Shift+Tab
|
|
126
|
+
* cycles backward.
|
|
127
|
+
*/ const handleKeyDown = useCallback((event)=>{
|
|
128
|
+
if (event.key !== "Tab" || !containerRef.current) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const activeElement = document.activeElement;
|
|
132
|
+
// If focus is in a nested dialog, let that dialog handle Tab.
|
|
133
|
+
if (activeElement && !containerRef.current.contains(activeElement) && activeElement.closest('[role="dialog"]')) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const focusableElements = getFocusableElements();
|
|
137
|
+
if (focusableElements.length === 0) {
|
|
138
|
+
event.preventDefault();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const firstElement = focusableElements[0];
|
|
142
|
+
const lastElement = focusableElements[focusableElements.length - 1];
|
|
143
|
+
// Shift+Tab from first element -> go to last.
|
|
144
|
+
if (event.shiftKey && activeElement === firstElement) {
|
|
145
|
+
event.preventDefault();
|
|
146
|
+
lastElement?.focus();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// Tab from last element -> go to first.
|
|
150
|
+
if (!event.shiftKey && activeElement === lastElement) {
|
|
151
|
+
event.preventDefault();
|
|
152
|
+
firstElement?.focus();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
/* c8 ignore next 9 - defensive check for focus escaping, hard to trigger in tests */ // If focus is outside the container, bring it back.
|
|
156
|
+
if (!containerRef.current.contains(activeElement)) {
|
|
157
|
+
event.preventDefault();
|
|
158
|
+
if (event.shiftKey) {
|
|
159
|
+
lastElement?.focus();
|
|
160
|
+
} else {
|
|
161
|
+
firstElement?.focus();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}, [
|
|
165
|
+
getFocusableElements
|
|
166
|
+
]);
|
|
167
|
+
/**
|
|
168
|
+
* Handle focus events to ensure focus stays within the container. This catches
|
|
169
|
+
* focus that escapes via mouse clicks or other means.
|
|
170
|
+
*/ const handleFocusIn = useCallback((event)=>{
|
|
171
|
+
if (!containerRef.current || containerRef.current.contains(event.target)) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
// Allow focus to go to nested dialogs (child panels).
|
|
175
|
+
const target = event.target;
|
|
176
|
+
if (target?.closest('[role="dialog"]')) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
// Focus escaped the container, bring it back.
|
|
180
|
+
const focusableElements = getFocusableElements();
|
|
181
|
+
if (focusableElements.length > 0) {
|
|
182
|
+
focusableElements[0]?.focus();
|
|
183
|
+
}
|
|
184
|
+
}, [
|
|
185
|
+
getFocusableElements
|
|
186
|
+
]);
|
|
187
|
+
// Set up focus trap when enabled.
|
|
188
|
+
useEffect(()=>{
|
|
189
|
+
if (!enabled) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
// Store the currently focused element to restore later.
|
|
193
|
+
previouslyFocusedRef.current = document.activeElement;
|
|
194
|
+
// Set initial focus after a small delay to ensure the DOM is ready.
|
|
195
|
+
const focusTimer = setTimeout(()=>{
|
|
196
|
+
focusElement(initialFocus);
|
|
197
|
+
}, 0);
|
|
198
|
+
// Add event listeners for focus trapping.
|
|
199
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
200
|
+
document.addEventListener("focusin", handleFocusIn);
|
|
201
|
+
return ()=>{
|
|
202
|
+
clearTimeout(focusTimer);
|
|
203
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
204
|
+
document.removeEventListener("focusin", handleFocusIn);
|
|
205
|
+
// Restore focus to the previously focused element if it's still in the DOM.
|
|
206
|
+
if (previouslyFocusedRef.current?.isConnected) {
|
|
207
|
+
previouslyFocusedRef.current.focus();
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}, [
|
|
211
|
+
enabled,
|
|
212
|
+
initialFocus,
|
|
213
|
+
focusElement,
|
|
214
|
+
handleKeyDown,
|
|
215
|
+
handleFocusIn
|
|
216
|
+
]);
|
|
217
|
+
return {
|
|
218
|
+
containerRef
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
68
222
|
;// CONCATENATED MODULE: ./src/hooks/useHaptic.ts
|
|
69
223
|
|
|
70
224
|
const HAPTIC_DURATION_MS = 50;
|
|
@@ -460,83 +614,211 @@ function useHotkeys(hotkeys, tagsToIgnore = [
|
|
|
460
614
|
|
|
461
615
|
;// CONCATENATED MODULE: ./src/hooks/useLocalStorage.ts
|
|
462
616
|
|
|
463
|
-
|
|
617
|
+
/**
|
|
618
|
+
* Dispatches a custom storage event to notify other parts of the application
|
|
619
|
+
* about localStorage changes. This is necessary because the native storage
|
|
620
|
+
* event only fires in other tabs/windows, not in the current one. By manually
|
|
621
|
+
* dispatching the event, we ensure that all components using the same key stay
|
|
622
|
+
* synchronized.
|
|
623
|
+
*
|
|
624
|
+
* @param key - The localStorage key that was modified
|
|
625
|
+
* @param newValue - The new value (stringified) or null if the item was removed
|
|
626
|
+
*
|
|
627
|
+
*/ function dispatchStorageEvent(key, newValue) {
|
|
464
628
|
window.dispatchEvent(new StorageEvent("storage", {
|
|
465
629
|
key,
|
|
466
630
|
newValue
|
|
467
631
|
}));
|
|
468
632
|
}
|
|
469
|
-
|
|
470
|
-
|
|
633
|
+
/**
|
|
634
|
+
* Sets an item in localStorage and dispatches a storage event. Handles both
|
|
635
|
+
* direct values and function updaters (similar to React's setState). Values are
|
|
636
|
+
* automatically serialized to JSON before storage.
|
|
637
|
+
*
|
|
638
|
+
* @param key - The localStorage key to set
|
|
639
|
+
* @param value - The value to store, or a function that returns the value to store
|
|
640
|
+
*
|
|
641
|
+
*/ const setLocalStorageItem = (key, value)=>{
|
|
642
|
+
/**
|
|
643
|
+
* If value is a function, call it to get the actual value (supports functional
|
|
644
|
+
* updates).
|
|
645
|
+
*/ const stringifiedValue = JSON.stringify(typeof value === "function" ? value() : value);
|
|
471
646
|
window.localStorage.setItem(key, stringifiedValue);
|
|
647
|
+
// Dispatch event to notify other components in the same window/tab.
|
|
472
648
|
dispatchStorageEvent(key, stringifiedValue);
|
|
473
649
|
};
|
|
474
|
-
|
|
650
|
+
/**
|
|
651
|
+
* Removes an item from localStorage and dispatches a storage event with null
|
|
652
|
+
* value. This ensures all components using this key are notified of the
|
|
653
|
+
* removal.
|
|
654
|
+
*
|
|
655
|
+
* @param key - The localStorage key to remove
|
|
656
|
+
*
|
|
657
|
+
*/ const removeLocalStorageItem = (key)=>{
|
|
475
658
|
window.localStorage.removeItem(key);
|
|
659
|
+
// Dispatch event with null to signal removal.
|
|
476
660
|
dispatchStorageEvent(key, null);
|
|
477
661
|
};
|
|
478
|
-
|
|
662
|
+
/**
|
|
663
|
+
* Retrieves an item from localStorage. Returns the raw stringified value or
|
|
664
|
+
* null if the key doesn't exist.
|
|
665
|
+
*
|
|
666
|
+
* @param key - The localStorage key to retrieve
|
|
667
|
+
* @returns The stored string value or null if not found
|
|
668
|
+
*
|
|
669
|
+
*/ const getLocalStorageItem = (key)=>{
|
|
479
670
|
return window.localStorage.getItem(key);
|
|
480
671
|
};
|
|
481
|
-
|
|
672
|
+
/**
|
|
673
|
+
* Creates a subscription to localStorage changes for use with React's
|
|
674
|
+
* useSyncExternalStore. This function is called by React to set up and tear
|
|
675
|
+
* down event listeners. It listens to both native storage events (from other
|
|
676
|
+
* tabs) and custom dispatched events (from this tab).
|
|
677
|
+
*
|
|
678
|
+
* @param callback - The callback function to invoke when storage changes occur
|
|
679
|
+
* @returns A cleanup function that removes the event listener
|
|
680
|
+
*
|
|
681
|
+
*/ const useLocalStorageSubscribe = (callback)=>{
|
|
482
682
|
window.addEventListener("storage", callback);
|
|
683
|
+
// Return cleanup function for React to call on unmount.
|
|
483
684
|
return ()=>window.removeEventListener("storage", callback);
|
|
484
685
|
};
|
|
485
686
|
/**
|
|
687
|
+
* A React hook for managing state synchronized with localStorage. Uses React
|
|
688
|
+
* 19's useSyncExternalStore for optimal concurrent rendering support and
|
|
689
|
+
* automatic synchronization across components. Changes are automatically
|
|
690
|
+
* persisted to localStorage and synchronized across all components using the
|
|
691
|
+
* same key, including across browser tabs.
|
|
692
|
+
*
|
|
693
|
+
* Features:
|
|
694
|
+
* - Automatic serialization/deserialization with JSON
|
|
695
|
+
* - Type-safe with TypeScript generics
|
|
696
|
+
* - Supports functional updates (similar to setState)
|
|
697
|
+
* - Synchronized across components and browser tabs
|
|
698
|
+
* - Handles edge cases (null, undefined, errors)
|
|
699
|
+
* - Compatible with React 19+ concurrent features
|
|
700
|
+
*
|
|
701
|
+
* @template T - The type of the value stored in localStorage
|
|
702
|
+
* @param {StorageProperties<T>} config - Configuration object with key and optional initialValue
|
|
703
|
+
* @returns {[T | null, (value: T | ((current: T) => T)) => void, () => void, () => void]} A tuple containing:
|
|
704
|
+
* - [0] current value from localStorage (or null if not set)
|
|
705
|
+
* - [1] setValue function to update the stored value (supports direct value or function updater)
|
|
706
|
+
* - [2] resetValue function to restore the initialValue
|
|
707
|
+
* - [3] removeValue function to remove the value from localStorage
|
|
486
708
|
*
|
|
487
709
|
* @example
|
|
710
|
+
* ```js
|
|
711
|
+
* // Basic usage with a string value
|
|
488
712
|
* import { useLocalStorage } from '@versini/ui-hooks';
|
|
489
|
-
* const [
|
|
713
|
+
* const [model, setModel, resetModel, removeModel] = useLocalStorage({
|
|
490
714
|
* key: 'gpt-model',
|
|
491
715
|
* initialValue: 'gpt-3',
|
|
492
716
|
* });
|
|
493
717
|
*
|
|
494
|
-
*
|
|
495
|
-
*
|
|
496
|
-
*
|
|
497
|
-
*
|
|
718
|
+
* // Direct update
|
|
719
|
+
* setModel('gpt-4'); // Stores "gpt-4"
|
|
720
|
+
*
|
|
721
|
+
* // Functional update (receives current value)
|
|
722
|
+
* setModel((current) => (current === 'gpt-3' ? 'gpt-4' : 'gpt-3'));
|
|
723
|
+
*
|
|
724
|
+
* // Reset to initial value
|
|
725
|
+
* resetModel(); // Restores "gpt-3"
|
|
726
|
+
*
|
|
727
|
+
* // Remove from localStorage
|
|
728
|
+
* removeModel(); // Sets value to null and removes from storage
|
|
729
|
+
* ```
|
|
730
|
+
*
|
|
731
|
+
* @example
|
|
732
|
+
* ```js
|
|
733
|
+
* // Usage with complex objects
|
|
734
|
+
* interface UserPreferences {
|
|
735
|
+
* theme: 'light' | 'dark';
|
|
736
|
+
* fontSize: number;
|
|
737
|
+
* }
|
|
738
|
+
*
|
|
739
|
+
* const [prefs, setPrefs] = useLocalStorage<UserPreferences>({
|
|
740
|
+
* key: 'user-preferences',
|
|
741
|
+
* initialValue: { theme: 'light', fontSize: 14 }
|
|
742
|
+
* });
|
|
743
|
+
*
|
|
744
|
+
* // Update specific property
|
|
745
|
+
* setPrefs(current => ({ ...current, theme: 'dark' }));
|
|
746
|
+
* ```
|
|
747
|
+
*
|
|
498
748
|
*/ function useLocalStorage({ key, initialValue }) {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
749
|
+
/**
|
|
750
|
+
* Snapshot function for useSyncExternalStore - returns current localStorage
|
|
751
|
+
* value.
|
|
752
|
+
*/ const getSnapshot = ()=>getLocalStorageItem(key);
|
|
753
|
+
/**
|
|
754
|
+
* Use React's useSyncExternalStore to subscribe to localStorage changes. This
|
|
755
|
+
* ensures proper integration with React 19+ concurrent rendering and provides
|
|
756
|
+
* automatic re-rendering when the stored value changes (either from this
|
|
757
|
+
* component, other components, or other browser tabs).
|
|
758
|
+
*/ const store = useSyncExternalStore(useLocalStorageSubscribe, getSnapshot);
|
|
759
|
+
/**
|
|
760
|
+
* Updates the stored value in localStorage. Accepts either a direct value or a
|
|
761
|
+
* function that receives the current value. Setting to null or undefined will
|
|
762
|
+
* remove the item from localStorage.
|
|
763
|
+
*/ const setValue = useCallback((v)=>{
|
|
502
764
|
try {
|
|
765
|
+
// Support both direct values and functional updates.
|
|
503
766
|
const nextState = typeof v === "function" ? v(JSON.parse(store)) : v;
|
|
767
|
+
// Remove from storage if value is null or undefined.
|
|
504
768
|
if (nextState === undefined || nextState === null) {
|
|
505
769
|
removeLocalStorageItem(key);
|
|
506
770
|
} else {
|
|
507
771
|
setLocalStorageItem(key, nextState);
|
|
508
772
|
}
|
|
509
|
-
/* v8 ignore next
|
|
773
|
+
/* v8 ignore next 4 */ } catch (e) {
|
|
774
|
+
// Log parsing or storage errors without breaking the application.
|
|
510
775
|
console.warn(e);
|
|
511
776
|
}
|
|
512
777
|
}, [
|
|
513
778
|
key,
|
|
514
779
|
store
|
|
515
780
|
]);
|
|
516
|
-
|
|
781
|
+
/**
|
|
782
|
+
* Resets the stored value back to the initialValue provided in the
|
|
783
|
+
* configuration. If no initialValue was provided, this will remove the item
|
|
784
|
+
* from localStorage.
|
|
785
|
+
*/ const resetValue = useCallback(()=>{
|
|
517
786
|
setValue(initialValue);
|
|
518
787
|
}, [
|
|
519
788
|
initialValue,
|
|
520
789
|
setValue
|
|
521
790
|
]);
|
|
522
|
-
|
|
791
|
+
/**
|
|
792
|
+
* Removes the value from localStorage entirely. After calling this, the hook
|
|
793
|
+
* will return null until a new value is set.
|
|
794
|
+
*/ const removeValue = useCallback(()=>{
|
|
523
795
|
setValue(null);
|
|
524
796
|
}, [
|
|
525
797
|
setValue
|
|
526
798
|
]);
|
|
527
|
-
|
|
799
|
+
/**
|
|
800
|
+
* Initialize localStorage with the initialValue on first mount if the key
|
|
801
|
+
* doesn't exist. This effect only runs once when the component mounts and
|
|
802
|
+
* ensures that the initialValue is persisted to localStorage if no value is
|
|
803
|
+
* currently stored.
|
|
804
|
+
*/ useEffect(()=>{
|
|
528
805
|
try {
|
|
806
|
+
// Only set initialValue if key doesn't exist and initialValue is defined.
|
|
529
807
|
if (getLocalStorageItem(key) === null && typeof initialValue !== "undefined") {
|
|
530
808
|
setLocalStorageItem(key, initialValue);
|
|
531
809
|
}
|
|
532
|
-
/* v8 ignore next
|
|
810
|
+
/* v8 ignore next 4 */ } catch (e) {
|
|
811
|
+
// Log initialization errors without breaking the application.
|
|
533
812
|
console.warn(e);
|
|
534
813
|
}
|
|
535
814
|
}, [
|
|
536
815
|
key,
|
|
537
816
|
initialValue
|
|
538
817
|
]);
|
|
539
|
-
|
|
818
|
+
/**
|
|
819
|
+
* Return tuple: [currentValue, setValue, resetValue, removeValue] Parse the
|
|
820
|
+
* stored JSON string back to its original type, or return null if empty.
|
|
821
|
+
*/ return [
|
|
540
822
|
store ? JSON.parse(store) : null,
|
|
541
823
|
setValue,
|
|
542
824
|
resetValue,
|
|
@@ -827,4 +1109,5 @@ function useWindowEvent(type, listener) {
|
|
|
827
1109
|
|
|
828
1110
|
|
|
829
1111
|
|
|
830
|
-
|
|
1112
|
+
|
|
1113
|
+
export { getHotkeyHandler, shouldFireEvent, useClickOutside, useFocusTrap, useHaptic, useHotkeys, useInViewport, useInterval, useIsMounted, useLocalStorage, useMergeRefs, useResizeObserver, useUncontrolled, useUniqueId, useViewportSize, useVisualViewportSize };
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@versini/ui-hooks",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Arno Versini",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
8
8
|
},
|
|
9
|
-
"homepage": "https://
|
|
9
|
+
"homepage": "https://www.npmjs.com/package/@versini/ui-hooks",
|
|
10
10
|
"repository": {
|
|
11
11
|
"type": "git",
|
|
12
12
|
"url": "git@github.com:aversini/ui-components.git"
|
|
@@ -36,5 +36,5 @@
|
|
|
36
36
|
"test:watch": "vitest",
|
|
37
37
|
"test": "vitest run"
|
|
38
38
|
},
|
|
39
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "15f3028acf21328c70773147a6bedfbafaa3a2a4"
|
|
40
40
|
}
|