@versini/ui-hooks 5.0.1 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -0
- package/dist/hooks/useHaptic.js +49 -0
- package/dist/index.d.ts +21 -1
- package/dist/index.js +25 -23
- package/package.json +2 -6
package/README.md
CHANGED
|
@@ -45,6 +45,7 @@ npm install @versini/ui-hooks
|
|
|
45
45
|
|
|
46
46
|
- **`useClickOutside`** - Detect clicks outside an element
|
|
47
47
|
- **`useHotkeys`** - Handle keyboard shortcuts and hotkeys
|
|
48
|
+
- **`useHaptic`** - Provide haptic feedback for mobile devices
|
|
48
49
|
|
|
49
50
|
### Storage Hooks
|
|
50
51
|
|
|
@@ -115,6 +116,42 @@ useHotkeys(
|
|
|
115
116
|
- `tagsToIgnore`: HTML tags to ignore (default: `["INPUT", "TEXTAREA", "SELECT"]`)
|
|
116
117
|
- `triggerOnContentEditable`: Whether to trigger on contentEditable elements
|
|
117
118
|
|
|
119
|
+
### useHaptic
|
|
120
|
+
|
|
121
|
+
Provide haptic feedback for mobile devices using the Vibration API or iOS switch element fallback.
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
const { haptic } = useHaptic(): { haptic: (count?: number) => void }
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Parameters:**
|
|
128
|
+
|
|
129
|
+
- `count` (optional): Number of haptic pulses to trigger (default: 1)
|
|
130
|
+
|
|
131
|
+
**Returns:** Object with `haptic` function to trigger feedback
|
|
132
|
+
|
|
133
|
+
**Example:**
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
import { useHaptic } from "@versini/ui-hooks";
|
|
137
|
+
|
|
138
|
+
function HapticButton() {
|
|
139
|
+
const { haptic } = useHaptic();
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<button onClick={() => haptic(1)}>Tap me (with haptic feedback)</button>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Notes:**
|
|
148
|
+
|
|
149
|
+
- Uses `navigator.vibrate` when available
|
|
150
|
+
- Falls back to iOS switch element trick for Safari on iOS
|
|
151
|
+
- Haptic duration: 50ms per pulse
|
|
152
|
+
- Interval between pulses: 120ms
|
|
153
|
+
- Multiple pulses create a vibration pattern for better UX
|
|
154
|
+
|
|
118
155
|
### useLocalStorage
|
|
119
156
|
|
|
120
157
|
Manage state synchronized with localStorage.
|
|
@@ -435,6 +472,46 @@ function ResizablePanel({ children }) {
|
|
|
435
472
|
}
|
|
436
473
|
```
|
|
437
474
|
|
|
475
|
+
### Haptic Feedback for Interactive UI
|
|
476
|
+
|
|
477
|
+
```tsx
|
|
478
|
+
import { useHaptic } from "@versini/ui-hooks";
|
|
479
|
+
|
|
480
|
+
function InteractiveCounter() {
|
|
481
|
+
const [count, setCount] = useState(0);
|
|
482
|
+
const { haptic } = useHaptic();
|
|
483
|
+
|
|
484
|
+
const increment = () => {
|
|
485
|
+
setCount((c) => c + 1);
|
|
486
|
+
haptic(1); // Single pulse
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
const decrement = () => {
|
|
490
|
+
setCount((c) => c - 1);
|
|
491
|
+
haptic(1); // Single pulse
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const reset = () => {
|
|
495
|
+
setCount(0);
|
|
496
|
+
haptic(2); // Double pulse for emphasis
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
const celebrate = () => {
|
|
500
|
+
haptic(3); // Triple pulse for celebration
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
return (
|
|
504
|
+
<div>
|
|
505
|
+
<h2>Count: {count}</h2>
|
|
506
|
+
<button onClick={increment}>+</button>
|
|
507
|
+
<button onClick={decrement}>-</button>
|
|
508
|
+
<button onClick={reset}>Reset</button>
|
|
509
|
+
{count >= 10 && <button onClick={celebrate}>🎉 Celebrate!</button>}
|
|
510
|
+
</div>
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
438
515
|
### Advanced Controlled/Uncontrolled Input
|
|
439
516
|
|
|
440
517
|
```tsx
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useRef as u, useEffect as d, useCallback as l } from "react";
|
|
2
|
+
const o = 50, s = 120, m = () => {
|
|
3
|
+
const c = u(null), r = u(null), i = u(/* @__PURE__ */ new Set());
|
|
4
|
+
d(() => typeof window > "u" ? void 0 : ((() => {
|
|
5
|
+
if (c.current && r.current)
|
|
6
|
+
return;
|
|
7
|
+
const e = document.createElement("input");
|
|
8
|
+
e.type = "checkbox", e.setAttribute("switch", ""), e.style.display = "none", e.setAttribute("aria-hidden", "true");
|
|
9
|
+
const t = document.createElement("label");
|
|
10
|
+
t.style.display = "none", t.setAttribute("aria-hidden", "true"), t.appendChild(e), document.body.appendChild(t), c.current = e, r.current = t;
|
|
11
|
+
})(), () => {
|
|
12
|
+
for (const e of i.current)
|
|
13
|
+
clearTimeout(e);
|
|
14
|
+
i.current.clear(), r.current && document.body.contains(r.current) && document.body.removeChild(r.current), c.current = null, r.current = null;
|
|
15
|
+
}), []);
|
|
16
|
+
const a = l(() => {
|
|
17
|
+
try {
|
|
18
|
+
if (navigator?.vibrate) {
|
|
19
|
+
navigator.vibrate(o);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
r.current?.click();
|
|
23
|
+
} catch {
|
|
24
|
+
}
|
|
25
|
+
}, []);
|
|
26
|
+
return { haptic: l(
|
|
27
|
+
(n = 1) => {
|
|
28
|
+
if (!(typeof window > "u") && !(n < 1)) {
|
|
29
|
+
if (navigator?.vibrate && n > 1) {
|
|
30
|
+
const e = [];
|
|
31
|
+
for (let t = 0; t < n; t++)
|
|
32
|
+
e.push(o), t < n - 1 && e.push(s - o);
|
|
33
|
+
navigator.vibrate(e);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
for (let e = 0; e < n; e++) {
|
|
37
|
+
const t = setTimeout(() => {
|
|
38
|
+
a(), i.current.delete(t);
|
|
39
|
+
}, e * s);
|
|
40
|
+
i.current.add(t);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
[a]
|
|
45
|
+
) };
|
|
46
|
+
};
|
|
47
|
+
export {
|
|
48
|
+
m as useHaptic
|
|
49
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,26 @@ import * as react from 'react';
|
|
|
18
18
|
*/
|
|
19
19
|
declare function useClickOutside<T extends HTMLElement = any>(handler: () => void, events?: string[] | null, nodes?: (HTMLElement | null)[]): react.RefObject<T | null>;
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Custom hook providing imperative haptic feedback for mobile devices. Uses
|
|
23
|
+
* navigator.vibrate when available, falls back to iOS switch element trick
|
|
24
|
+
* for Safari on iOS devices that don't support the Vibration API.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```tsx
|
|
28
|
+
* const { haptic } = useHaptic();
|
|
29
|
+
*
|
|
30
|
+
* // Trigger a single haptic pulse
|
|
31
|
+
* haptic(1);
|
|
32
|
+
*
|
|
33
|
+
* // Trigger two rapid haptic pulses
|
|
34
|
+
* haptic(2);
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
declare const useHaptic: () => {
|
|
38
|
+
haptic: (count?: number) => void;
|
|
39
|
+
};
|
|
40
|
+
|
|
21
41
|
interface HotkeyItemOptions {
|
|
22
42
|
preventDefault?: boolean;
|
|
23
43
|
}
|
|
@@ -223,4 +243,4 @@ declare function useVisualViewportSize(): {
|
|
|
223
243
|
height: number;
|
|
224
244
|
};
|
|
225
245
|
|
|
226
|
-
export { type HotkeyItem, type HotkeyItemOptions, type StorageProperties, type UseUniqueIdOptions, getHotkeyHandler, shouldFireEvent, useClickOutside, useHotkeys, useInViewport, useInterval, useIsMounted, useLocalStorage, useMergeRefs, useResizeObserver, useUncontrolled, useUniqueId, useViewportSize, useVisualViewportSize };
|
|
246
|
+
export { type HotkeyItem, type HotkeyItemOptions, type StorageProperties, type UseUniqueIdOptions, getHotkeyHandler, shouldFireEvent, useClickOutside, useHaptic, useHotkeys, useInViewport, useInterval, useIsMounted, useLocalStorage, useMergeRefs, useResizeObserver, useUncontrolled, useUniqueId, useViewportSize, useVisualViewportSize };
|
package/dist/index.js
CHANGED
|
@@ -1,32 +1,34 @@
|
|
|
1
1
|
import { useClickOutside as r } from "./hooks/useClickOutside.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
2
|
+
import { useHaptic as s } from "./hooks/useHaptic.js";
|
|
3
|
+
import { getHotkeyHandler as p, shouldFireEvent as f, useHotkeys as m } from "./hooks/useHotkeys.js";
|
|
4
|
+
import { useInterval as i } from "./hooks/useInterval.js";
|
|
5
|
+
import { useInViewport as n } from "./hooks/useInViewport.js";
|
|
6
|
+
import { useIsMounted as d } from "./hooks/useIsMounted.js";
|
|
7
|
+
import { useLocalStorage as H } from "./hooks/useLocalStorage.js";
|
|
7
8
|
import { useMergeRefs as V } from "./hooks/useMergeRefs.js";
|
|
8
|
-
import { useResizeObserver as
|
|
9
|
-
import { useUncontrolled as
|
|
10
|
-
import { useUniqueId as
|
|
11
|
-
import { useViewportSize as
|
|
12
|
-
import { useVisualViewportSize as
|
|
9
|
+
import { useResizeObserver as k } from "./hooks/useResizeObserver.js";
|
|
10
|
+
import { useUncontrolled as w } from "./hooks/useUncontrolled.js";
|
|
11
|
+
import { useUniqueId as S } from "./hooks/useUniqueId.js";
|
|
12
|
+
import { useViewportSize as M } from "./hooks/useViewportSize.js";
|
|
13
|
+
import { useVisualViewportSize as R } from "./hooks/useVisualViewportSize.js";
|
|
13
14
|
/*!
|
|
14
|
-
@versini/ui-hooks v5.0
|
|
15
|
+
@versini/ui-hooks v5.1.0
|
|
15
16
|
© 2025 gizmette.com
|
|
16
17
|
*/
|
|
17
18
|
export {
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
p as getHotkeyHandler,
|
|
20
|
+
f as shouldFireEvent,
|
|
20
21
|
r as useClickOutside,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
s as useHaptic,
|
|
23
|
+
m as useHotkeys,
|
|
24
|
+
n as useInViewport,
|
|
25
|
+
i as useInterval,
|
|
26
|
+
d as useIsMounted,
|
|
27
|
+
H as useLocalStorage,
|
|
26
28
|
V as useMergeRefs,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
k as useResizeObserver,
|
|
30
|
+
w as useUncontrolled,
|
|
31
|
+
S as useUniqueId,
|
|
32
|
+
M as useViewportSize,
|
|
33
|
+
R as useVisualViewportSize
|
|
32
34
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@versini/ui-hooks",
|
|
3
|
-
"version": "5.0
|
|
3
|
+
"version": "5.1.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Arno Versini",
|
|
6
6
|
"publishConfig": {
|
|
@@ -36,9 +36,5 @@
|
|
|
36
36
|
"test:watch": "vitest",
|
|
37
37
|
"test": "vitest run"
|
|
38
38
|
},
|
|
39
|
-
"
|
|
40
|
-
"react": "^19.1.0",
|
|
41
|
-
"react-dom": "^19.1.0"
|
|
42
|
-
},
|
|
43
|
-
"gitHead": "dcc216644c8c3e7d43a49ea655a22aed21fa4b83"
|
|
39
|
+
"gitHead": "e2e7b5c53c77d1773d7e0e463bac2a75b936a0d3"
|
|
44
40
|
}
|