@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/README.md CHANGED
@@ -32,6 +32,26 @@ npm install @versini/ui-hooks
32
32
 
33
33
  > **Note**: While this package contains React hooks without styling, when used alongside the UI component packages it assumes TailwindCSS and the `@versini/ui-styles` plugin are configured. See the [installation documentation](https://versini-org.github.io/ui-components/?path=/docs/getting-started-installation--docs) for complete setup instructions.
34
34
 
35
+ ## Usage
36
+
37
+ Each hook is exported from its own subpath for optimal tree-shaking:
38
+
39
+ ```tsx
40
+ import { useUniqueId } from "@versini/ui-hooks/use-unique-id";
41
+ import { useClickOutside } from "@versini/ui-hooks/use-click-outside";
42
+ import { useHotkeys, getHotkeyHandler } from "@versini/ui-hooks/use-hotkeys";
43
+ import { useHaptic } from "@versini/ui-hooks/use-haptic";
44
+ import { useLocalStorage } from "@versini/ui-hooks/use-local-storage";
45
+ import { useViewportSize } from "@versini/ui-hooks/use-viewport-size";
46
+ import { useVisualViewportSize } from "@versini/ui-hooks/use-visual-viewport-size";
47
+ import { useInViewport } from "@versini/ui-hooks/use-in-viewport";
48
+ import { useResizeObserver } from "@versini/ui-hooks/use-resize-observer";
49
+ import { useInterval } from "@versini/ui-hooks/use-interval";
50
+ import { useIsMounted } from "@versini/ui-hooks/use-is-mounted";
51
+ import { useMergeRefs } from "@versini/ui-hooks/use-merge-refs";
52
+ import { useUncontrolled } from "@versini/ui-hooks/use-uncontrolled";
53
+ ```
54
+
35
55
  ## Available Hooks
36
56
 
37
57
  ### Core Utility Hooks
@@ -133,7 +153,7 @@ const { haptic } = useHaptic(): { haptic: (count?: number) => void }
133
153
  **Example:**
134
154
 
135
155
  ```tsx
136
- import { useHaptic } from "@versini/ui-hooks";
156
+ import { useHaptic } from "@versini/ui-hooks/use-haptic";
137
157
 
138
158
  function HapticButton() {
139
159
  const { haptic } = useHaptic();
@@ -285,7 +305,7 @@ const [value, setValue, isControlled] = useUncontrolled<T>({
285
305
  ### Accessible Form Field
286
306
 
287
307
  ```tsx
288
- import { useUniqueId } from "@versini/ui-hooks";
308
+ import { useUniqueId } from "@versini/ui-hooks/use-unique-id";
289
309
 
290
310
  function FormField({ label, helpText, error, ...props }) {
291
311
  const fieldId = useUniqueId("field");
@@ -317,7 +337,9 @@ function FormField({ label, helpText, error, ...props }) {
317
337
  ### Modal with Click Outside and Hotkeys
318
338
 
319
339
  ```tsx
320
- import { useClickOutside, useHotkeys, useUniqueId } from "@versini/ui-hooks";
340
+ import { useClickOutside } from "@versini/ui-hooks/use-click-outside";
341
+ import { useHotkeys } from "@versini/ui-hooks/use-hotkeys";
342
+ import { useUniqueId } from "@versini/ui-hooks/use-unique-id";
321
343
 
322
344
  function Modal({ isOpen, onClose, title, children }) {
323
345
  const titleId = useUniqueId("modal-title");
@@ -349,7 +371,8 @@ function Modal({ isOpen, onClose, title, children }) {
349
371
  ### Responsive Component with Viewport Tracking
350
372
 
351
373
  ```tsx
352
- import { useViewportSize, useVisualViewportSize } from "@versini/ui-hooks";
374
+ import { useViewportSize } from "@versini/ui-hooks/use-viewport-size";
375
+ import { useVisualViewportSize } from "@versini/ui-hooks/use-visual-viewport-size";
353
376
 
354
377
  function ResponsiveComponent() {
355
378
  const viewport = useViewportSize();
@@ -376,7 +399,9 @@ function ResponsiveComponent() {
376
399
  ### Auto-Save with Local Storage and Intervals
377
400
 
378
401
  ```tsx
379
- import { useLocalStorage, useInterval, useIsMounted } from "@versini/ui-hooks";
402
+ import { useLocalStorage } from "@versini/ui-hooks/use-local-storage";
403
+ import { useInterval } from "@versini/ui-hooks/use-interval";
404
+ import { useIsMounted } from "@versini/ui-hooks/use-is-mounted";
380
405
 
381
406
  function AutoSaveEditor() {
382
407
  const [content, setContent] = useLocalStorage({
@@ -420,7 +445,7 @@ function AutoSaveEditor() {
420
445
  ### Lazy Loading with Intersection Observer
421
446
 
422
447
  ```tsx
423
- import { useInViewport } from "@versini/ui-hooks";
448
+ import { useInViewport } from "@versini/ui-hooks/use-in-viewport";
424
449
 
425
450
  function LazyImage({ src, alt, placeholder }) {
426
451
  const { ref, inViewport } = useInViewport();
@@ -447,7 +472,7 @@ function LazyImage({ src, alt, placeholder }) {
447
472
  ### Resizable Panel with Size Tracking
448
473
 
449
474
  ```tsx
450
- import { useResizeObserver } from "@versini/ui-hooks";
475
+ import { useResizeObserver } from "@versini/ui-hooks/use-resize-observer";
451
476
 
452
477
  function ResizablePanel({ children }) {
453
478
  const [ref, rect] = useResizeObserver();
@@ -475,7 +500,7 @@ function ResizablePanel({ children }) {
475
500
  ### Haptic Feedback for Interactive UI
476
501
 
477
502
  ```tsx
478
- import { useHaptic } from "@versini/ui-hooks";
503
+ import { useHaptic } from "@versini/ui-hooks/use-haptic";
479
504
 
480
505
  function InteractiveCounter() {
481
506
  const [count, setCount] = useState(0);
@@ -515,7 +540,8 @@ function InteractiveCounter() {
515
540
  ### Advanced Controlled/Uncontrolled Input
516
541
 
517
542
  ```tsx
518
- import { useUncontrolled, useUniqueId } from "@versini/ui-hooks";
543
+ import { useUncontrolled } from "@versini/ui-hooks/use-uncontrolled";
544
+ import { useUniqueId } from "@versini/ui-hooks/use-unique-id";
519
545
 
520
546
  function AdvancedInput({ value, defaultValue, onChange, label, ...props }) {
521
547
  const [inputValue, setInputValue, isControlled] = useUncontrolled({
@@ -0,0 +1,21 @@
1
+ export declare class ResizeObserverMock {
2
+ private static _instances;
3
+ private callback;
4
+ private _observe;
5
+ private _unobserve;
6
+ private _disconnect;
7
+ constructor(callback: ResizeObserverCallback);
8
+ static get instances(): ResizeObserverMock[];
9
+ observe(element: Element): void;
10
+ unobserve(element: Element): void;
11
+ disconnect(): void;
12
+ mockTrigger(entries: ResizeObserverEntry[]): void;
13
+ simulateResize({ bottom, height, left, right, top, width, }?: {
14
+ bottom?: number;
15
+ height?: number;
16
+ left?: number;
17
+ right?: number;
18
+ top?: number;
19
+ width?: number;
20
+ }): void;
21
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Custom hooks that triggers a callback when a click is detected
3
+ * outside the target element.
4
+ *
5
+ * @param handler - Function to be called when clicked outside
6
+ * @param events - Array of events to listen to
7
+ * @param nodes - Array of nodes to check against
8
+ * @returns Ref to be attached to the target element
9
+ *
10
+ * @example
11
+ * const ref = useClickOutside(() => {
12
+ * console.log('Clicked outside!');
13
+ * });
14
+ * <div ref={ref}>Click me!</div>
15
+ *
16
+ */
17
+ export declare function useClickOutside<T extends HTMLElement = any>(handler: () => void, events?: string[] | null, nodes?: (HTMLElement | null)[]): import("react").RefObject<T | null>;
@@ -0,0 +1,68 @@
1
+ /*!
2
+ @versini/ui-hooks v6.0.1
3
+ © 2025 gizmette.com
4
+ */
5
+ try {
6
+ if (!window.__VERSINI_UI_HOOKS__) {
7
+ window.__VERSINI_UI_HOOKS__ = {
8
+ version: "6.0.1",
9
+ buildTime: "12/24/2025 09:19 AM 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 { useEffect, useRef } from "react";
19
+
20
+ ;// CONCATENATED MODULE: external "react"
21
+
22
+ ;// CONCATENATED MODULE: ./src/hooks/useClickOutside/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 start */ const target = event ? event.target : undefined;
48
+ if (Array.isArray(nodes)) {
49
+ const shouldIgnore = !document.body.contains(target) && target.tagName !== "HTML";
50
+ /* v8 ignore stop */ 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
+ export { useClickOutside };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Custom hook providing imperative haptic feedback for mobile devices. Uses
3
+ * navigator.vibrate when available, falls back to iOS switch element trick for
4
+ * Safari on iOS devices that don't support the Vibration API.
5
+ *
6
+ * This hook uses a singleton pattern - only one haptic element is created in
7
+ * the DOM regardless of how many components use this hook. The element is
8
+ * automatically cleaned up when the last component unmounts.
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * const { haptic } = useHaptic();
13
+ *
14
+ * // Trigger a single haptic pulse
15
+ * haptic(1);
16
+ *
17
+ * // Trigger two rapid haptic pulses
18
+ * haptic(2);
19
+ * ```
20
+ *
21
+ */
22
+ export declare const useHaptic: () => {
23
+ haptic: (count?: number) => void;
24
+ };
@@ -0,0 +1,200 @@
1
+ /*!
2
+ @versini/ui-hooks v6.0.1
3
+ © 2025 gizmette.com
4
+ */
5
+ try {
6
+ if (!window.__VERSINI_UI_HOOKS__) {
7
+ window.__VERSINI_UI_HOOKS__ = {
8
+ version: "6.0.1",
9
+ buildTime: "12/24/2025 09:19 AM 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, useRef } from "react";
19
+
20
+ ;// CONCATENATED MODULE: external "react"
21
+
22
+ ;// CONCATENATED MODULE: ./src/hooks/useHaptic/useHaptic.ts
23
+
24
+ const HAPTIC_DURATION_MS = 50;
25
+ const HAPTIC_INTERVAL_MS = 120;
26
+ /**
27
+ * Singleton state for the haptic element shared across all hook instances. This
28
+ * ensures only one DOM element is created regardless of how many components use
29
+ * the hook.
30
+ */ let sharedLabelElement = null;
31
+ let refCount = 0;
32
+ /**
33
+ * Creates the shared haptic element if it doesn't exist. This function is
34
+ * idempotent - calling it multiple times is safe. Checks DOM directly to handle
35
+ * React Strict Mode double-mounting.
36
+ */ const ensureHapticElement = ()=>{
37
+ /* v8 ignore start - SSR check */ if (typeof window === "undefined") {
38
+ return;
39
+ }
40
+ /* v8 ignore stop */ // First check: do we have valid references that exist in the DOM?
41
+ if (sharedLabelElement && document.body.contains(sharedLabelElement)) {
42
+ return;
43
+ }
44
+ /**
45
+ * Second check: does an element already exist in the DOM from a previous
46
+ * mount?
47
+ */ const existingLabel = document.querySelector('label[data-haptic-singleton="true"]');
48
+ if (existingLabel) {
49
+ sharedLabelElement = existingLabel;
50
+ return;
51
+ }
52
+ // Clear stale references.
53
+ sharedLabelElement = null;
54
+ // Create new elements.
55
+ const input = document.createElement("input");
56
+ input.type = "checkbox";
57
+ input.setAttribute("switch", "");
58
+ input.style.display = "none";
59
+ input.setAttribute("aria-hidden", "true");
60
+ input.dataset.hapticSingleton = "true";
61
+ const label = document.createElement("label");
62
+ label.style.display = "none";
63
+ label.setAttribute("aria-hidden", "true");
64
+ label.dataset.hapticSingleton = "true";
65
+ label.appendChild(input);
66
+ document.body.appendChild(label);
67
+ sharedLabelElement = label;
68
+ };
69
+ /**
70
+ * Removes the shared haptic element from the DOM and clears references. Only
71
+ * called when the last component using the hook unmounts.
72
+ */ const cleanupHapticElement = ()=>{
73
+ /* v8 ignore start - cleanup edge case */ if (sharedLabelElement && document.body && document.body.contains(sharedLabelElement)) {
74
+ document.body.removeChild(sharedLabelElement);
75
+ }
76
+ /* v8 ignore stop */ sharedLabelElement = null;
77
+ };
78
+ /**
79
+ * Custom hook providing imperative haptic feedback for mobile devices. Uses
80
+ * navigator.vibrate when available, falls back to iOS switch element trick for
81
+ * Safari on iOS devices that don't support the Vibration API.
82
+ *
83
+ * This hook uses a singleton pattern - only one haptic element is created in
84
+ * the DOM regardless of how many components use this hook. The element is
85
+ * automatically cleaned up when the last component unmounts.
86
+ *
87
+ * @example
88
+ * ```tsx
89
+ * const { haptic } = useHaptic();
90
+ *
91
+ * // Trigger a single haptic pulse
92
+ * haptic(1);
93
+ *
94
+ * // Trigger two rapid haptic pulses
95
+ * haptic(2);
96
+ * ```
97
+ *
98
+ */ const useHaptic = ()=>{
99
+ const timeoutsRef = useRef(new Set());
100
+ useEffect(()=>{
101
+ // Increment reference count and create element if needed.
102
+ refCount++;
103
+ try {
104
+ ensureHapticElement();
105
+ /* v8 ignore start - error handling for element creation failure */ } catch (error) {
106
+ /**
107
+ * If element creation fails, we need to decrement refCount immediately since
108
+ * the cleanup function won't be registered. This prevents refCount from
109
+ * getting out of sync.
110
+ */ refCount--;
111
+ throw error;
112
+ }
113
+ /* v8 ignore stop */ return ()=>{
114
+ /**
115
+ * Cleanup function: clear all pending timeouts to prevent haptics from
116
+ * firing after unmount, and decrement the reference count. Only remove the
117
+ * DOM element when the last component unmounts.
118
+ */ for (const timeoutId of timeoutsRef.current){
119
+ clearTimeout(timeoutId);
120
+ }
121
+ timeoutsRef.current.clear();
122
+ refCount--;
123
+ if (refCount === 0) {
124
+ cleanupHapticElement();
125
+ }
126
+ };
127
+ }, []);
128
+ /**
129
+ * Triggers a single haptic pulse using either the Vibration API or the switch
130
+ * element trick.
131
+ */ const triggerSingleHaptic = useCallback(()=>{
132
+ try {
133
+ if (navigator?.vibrate) {
134
+ navigator.vibrate(HAPTIC_DURATION_MS);
135
+ return;
136
+ }
137
+ sharedLabelElement?.click();
138
+ } catch {
139
+ // Silently fail if haptics are not supported.
140
+ }
141
+ }, []);
142
+ /**
143
+ * Triggers haptic feedback with the specified number of pulses.
144
+ *
145
+ * @param count - Number of haptic pulses to trigger (default: 1). For count > 1,
146
+ * pulses are triggered in rapid succession with a 120ms interval between each
147
+ * pulse.
148
+ *
149
+ * @example
150
+ * ```tsx
151
+ * const { haptic } = useHaptic();
152
+ *
153
+ * // Single haptic
154
+ * haptic(1);
155
+ *
156
+ * // Two rapid haptics
157
+ * haptic(2);
158
+ *
159
+ * // Three rapid haptics
160
+ * haptic(3);
161
+ * ```
162
+ *
163
+ */ const haptic = useCallback((count = 1)=>{
164
+ /* v8 ignore start - SSR check */ if (typeof window === "undefined") {
165
+ return;
166
+ }
167
+ /* v8 ignore stop */ if (count < 1) {
168
+ return;
169
+ }
170
+ if (navigator?.vibrate && count > 1) {
171
+ /**
172
+ * For multi-haptic patterns with navigator.vibrate, create an array pattern
173
+ * like [50, 120, 50, 120, 50] for better timing control.
174
+ */ const pattern = [];
175
+ for(let i = 0; i < count; i++){
176
+ pattern.push(HAPTIC_DURATION_MS);
177
+ if (i < count - 1) {
178
+ pattern.push(HAPTIC_INTERVAL_MS - HAPTIC_DURATION_MS);
179
+ }
180
+ }
181
+ navigator.vibrate(pattern);
182
+ return;
183
+ }
184
+ // For switch element or single vibration, trigger sequentially.
185
+ for(let i = 0; i < count; i++){
186
+ const timeoutId = setTimeout(()=>{
187
+ triggerSingleHaptic();
188
+ timeoutsRef.current.delete(timeoutId);
189
+ }, i * HAPTIC_INTERVAL_MS);
190
+ timeoutsRef.current.add(timeoutId);
191
+ }
192
+ }, [
193
+ triggerSingleHaptic
194
+ ]);
195
+ return {
196
+ haptic
197
+ };
198
+ };
199
+
200
+ export { useHaptic };
@@ -0,0 +1,10 @@
1
+ import { getHotkeyHandler, HotkeyItemOptions } from "./utilities";
2
+ export type { HotkeyItemOptions };
3
+ export { getHotkeyHandler };
4
+ export type HotkeyItem = [
5
+ string,
6
+ (event: KeyboardEvent) => void,
7
+ HotkeyItemOptions?
8
+ ];
9
+ export declare function shouldFireEvent(event: KeyboardEvent, tagsToIgnore: string[], triggerOnContentEditable?: boolean): boolean;
10
+ export declare function useHotkeys(hotkeys: HotkeyItem[], tagsToIgnore?: string[], triggerOnContentEditable?: boolean): void;
@@ -0,0 +1,65 @@
1
+ /*!
2
+ @versini/ui-hooks v6.0.1
3
+ © 2025 gizmette.com
4
+ */
5
+ try {
6
+ if (!window.__VERSINI_UI_HOOKS__) {
7
+ window.__VERSINI_UI_HOOKS__ = {
8
+ version: "6.0.1",
9
+ buildTime: "12/24/2025 09:19 AM 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 { useEffect } from "react";
19
+ import { getHotkeyHandler, getHotkeyMatcher } from "./utilities.js";
20
+
21
+ ;// CONCATENATED MODULE: external "react"
22
+
23
+ ;// CONCATENATED MODULE: external "./utilities.js"
24
+
25
+ ;// CONCATENATED MODULE: ./src/hooks/useHotkeys/useHotkeys.ts
26
+
27
+
28
+
29
+ function shouldFireEvent(event, tagsToIgnore, triggerOnContentEditable = false) {
30
+ if (event.target instanceof HTMLElement) {
31
+ if (triggerOnContentEditable) {
32
+ return !tagsToIgnore.includes(event.target.tagName);
33
+ }
34
+ return !event.target.isContentEditable && !tagsToIgnore.includes(event.target.tagName);
35
+ }
36
+ return true;
37
+ }
38
+ function useHotkeys(hotkeys, tagsToIgnore = [
39
+ "INPUT",
40
+ "TEXTAREA",
41
+ "SELECT"
42
+ ], triggerOnContentEditable = false) {
43
+ useEffect(()=>{
44
+ const keydownListener = (event)=>{
45
+ hotkeys.forEach(([hotkey, handler, options = {
46
+ preventDefault: true
47
+ }])=>{
48
+ if (getHotkeyMatcher(hotkey)(event) && shouldFireEvent(event, tagsToIgnore, triggerOnContentEditable)) {
49
+ /* v8 ignore start - preventDefault edge case */ if (options.preventDefault) {
50
+ event.preventDefault();
51
+ }
52
+ /* v8 ignore stop */ handler(event);
53
+ }
54
+ });
55
+ };
56
+ document.documentElement.addEventListener("keydown", keydownListener);
57
+ return ()=>document.documentElement.removeEventListener("keydown", keydownListener);
58
+ }, [
59
+ hotkeys,
60
+ tagsToIgnore,
61
+ triggerOnContentEditable
62
+ ]);
63
+ }
64
+
65
+ export { getHotkeyHandler, shouldFireEvent, useHotkeys };
@@ -0,0 +1,19 @@
1
+ export type KeyboardModifiers = {
2
+ alt: boolean;
3
+ ctrl: boolean;
4
+ meta: boolean;
5
+ mod: boolean;
6
+ shift: boolean;
7
+ };
8
+ export type Hotkey = KeyboardModifiers & {
9
+ key?: string;
10
+ };
11
+ type CheckHotkeyMatch = (event: KeyboardEvent) => boolean;
12
+ export declare function parseHotkey(hotkey: string): Hotkey;
13
+ export declare function getHotkeyMatcher(hotkey: string): CheckHotkeyMatch;
14
+ export interface HotkeyItemOptions {
15
+ preventDefault?: boolean;
16
+ }
17
+ type HotkeyItem = [string, (event: any) => void, HotkeyItemOptions?];
18
+ export declare function getHotkeyHandler(hotkeys: HotkeyItem[]): (event: React.KeyboardEvent<HTMLElement> | KeyboardEvent) => void;
19
+ export {};
@@ -0,0 +1,87 @@
1
+ /*!
2
+ @versini/ui-hooks v6.0.1
3
+ © 2025 gizmette.com
4
+ */
5
+ try {
6
+ if (!window.__VERSINI_UI_HOOKS__) {
7
+ window.__VERSINI_UI_HOOKS__ = {
8
+ version: "6.0.1",
9
+ buildTime: "12/24/2025 09:19 AM 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
+
19
+ ;// CONCATENATED MODULE: ./src/hooks/useHotkeys/utilities.ts
20
+ function parseHotkey(hotkey) {
21
+ const keys = hotkey.toLowerCase().split("+").map((part)=>part.trim());
22
+ const modifiers = {
23
+ alt: keys.includes("alt"),
24
+ ctrl: keys.includes("ctrl"),
25
+ meta: keys.includes("meta"),
26
+ mod: keys.includes("mod"),
27
+ shift: keys.includes("shift")
28
+ };
29
+ const reservedKeys = [
30
+ "alt",
31
+ "ctrl",
32
+ "meta",
33
+ "shift",
34
+ "mod"
35
+ ];
36
+ const freeKey = keys.find((key)=>!reservedKeys.includes(key));
37
+ return {
38
+ ...modifiers,
39
+ key: freeKey
40
+ };
41
+ }
42
+ function isExactHotkey(hotkey, event) {
43
+ const { alt, ctrl, meta, mod, shift, key } = hotkey;
44
+ const { altKey, ctrlKey, metaKey, shiftKey, key: pressedKey } = event;
45
+ if (alt !== altKey) {
46
+ return false;
47
+ }
48
+ if (mod) {
49
+ if (!ctrlKey && !metaKey) {
50
+ return false;
51
+ }
52
+ } else {
53
+ if (ctrl !== ctrlKey) {
54
+ return false;
55
+ }
56
+ if (meta !== metaKey) {
57
+ return false;
58
+ }
59
+ }
60
+ if (shift !== shiftKey) {
61
+ return false;
62
+ }
63
+ if (key && (pressedKey.toLowerCase() === key.toLowerCase() || event.code.replace("Key", "").toLowerCase() === key.toLowerCase())) {
64
+ return true;
65
+ }
66
+ return false;
67
+ }
68
+ function getHotkeyMatcher(hotkey) {
69
+ return (event)=>isExactHotkey(parseHotkey(hotkey), event);
70
+ }
71
+ function getHotkeyHandler(hotkeys) {
72
+ return (event)=>{
73
+ const _event = "nativeEvent" in event ? event.nativeEvent : event;
74
+ /* v8 ignore start - hotkey handler matching */ hotkeys.forEach(([hotkey, handler, options = {
75
+ preventDefault: true
76
+ }])=>{
77
+ if (getHotkeyMatcher(hotkey)(_event)) {
78
+ if (options.preventDefault) {
79
+ event.preventDefault();
80
+ }
81
+ handler(_event);
82
+ }
83
+ });
84
+ /* v8 ignore stop */ };
85
+ }
86
+
87
+ export { getHotkeyHandler, getHotkeyMatcher, parseHotkey };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Hook that checks if an element is visible in the viewport.
3
+ * @returns
4
+ * ref: React ref object to attach to the element you want to monitor.
5
+ * inViewport: Boolean indicating if the element is in the viewport.
6
+ */
7
+ export declare function useInViewport<T extends HTMLElement = any>(): {
8
+ ref: (node: T | null) => void;
9
+ inViewport: boolean;
10
+ };