@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.
- package/README.md +35 -9
- package/dist/__tests__/__mocks__/ResizeObserver.d.ts +21 -0
- package/dist/__tests__/useClickOutside.test.d.ts +1 -0
- package/dist/__tests__/useHaptic.test.d.ts +1 -0
- package/dist/__tests__/useHotkeys.test.d.ts +1 -0
- package/dist/__tests__/useInterval.test.d.ts +1 -0
- package/dist/__tests__/useIsMounted.test.d.ts +1 -0
- package/dist/__tests__/useLocalStorage.test.d.ts +1 -0
- package/dist/__tests__/useMergeRefs.test.d.ts +1 -0
- package/dist/__tests__/useResizeObserver.test.d.ts +1 -0
- package/dist/__tests__/useUncontrolled.test.d.ts +1 -0
- package/dist/__tests__/useUniqueId.test.d.ts +1 -0
- package/dist/__tests__/useViewportSize.test.d.ts +1 -0
- package/dist/__tests__/utilities.test.d.ts +1 -0
- package/dist/useClickOutside/useClickOutside.d.ts +17 -0
- package/dist/useClickOutside/useClickOutside.js +68 -0
- package/dist/useHaptic/useHaptic.d.ts +24 -0
- package/dist/useHaptic/useHaptic.js +200 -0
- package/dist/useHotkeys/useHotkeys.d.ts +10 -0
- package/dist/useHotkeys/useHotkeys.js +65 -0
- package/dist/useHotkeys/utilities.d.ts +19 -0
- package/dist/useHotkeys/utilities.js +87 -0
- package/dist/useInViewport/useInViewport.d.ts +10 -0
- package/dist/useInViewport/useInViewport.js +52 -0
- package/dist/useInterval/useInterval.d.ts +21 -0
- package/dist/useInterval/useInterval.js +75 -0
- package/dist/useIsMounted/useIsMounted.d.ts +13 -0
- package/dist/useIsMounted/useIsMounted.js +46 -0
- package/dist/useLocalStorage/useLocalStorage.d.ts +82 -0
- package/dist/useLocalStorage/useLocalStorage.js +236 -0
- package/dist/useMergeRefs/useMergeRefs.d.ts +20 -0
- package/dist/useMergeRefs/useMergeRefs.js +62 -0
- package/dist/useResizeObserver/useResizeObserver.d.ts +17 -0
- package/dist/useResizeObserver/useResizeObserver.js +94 -0
- package/dist/useUncontrolled/useUncontrolled.d.ts +14 -0
- package/dist/useUncontrolled/useUncontrolled.js +76 -0
- package/dist/useUniqueId/useUniqueId.d.ts +36 -0
- package/dist/useUniqueId/useUniqueId.js +41 -0
- package/dist/useViewportSize/useViewportSize.d.ts +13 -0
- package/dist/useViewportSize/useViewportSize.js +60 -0
- package/dist/useVisualViewportSize/useVisualViewportSize.d.ts +14 -0
- package/dist/useVisualViewportSize/useVisualViewportSize.js +70 -0
- package/package.json +56 -4
- package/dist/index.d.ts +0 -316
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
};
|