@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 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 { getHotkeyHandler as s, shouldFireEvent as u, useHotkeys as p } from "./hooks/useHotkeys.js";
3
- import { useInterval as m } from "./hooks/useInterval.js";
4
- import { useInViewport as i } from "./hooks/useInViewport.js";
5
- import { useIsMounted as n } from "./hooks/useIsMounted.js";
6
- import { useLocalStorage as a } from "./hooks/useLocalStorage.js";
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 g } from "./hooks/useResizeObserver.js";
9
- import { useUncontrolled as v } from "./hooks/useUncontrolled.js";
10
- import { useUniqueId as z } from "./hooks/useUniqueId.js";
11
- import { useViewportSize as S } from "./hooks/useViewportSize.js";
12
- import { useVisualViewportSize as M } from "./hooks/useVisualViewportSize.js";
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.1
15
+ @versini/ui-hooks v5.1.0
15
16
  © 2025 gizmette.com
16
17
  */
17
18
  export {
18
- s as getHotkeyHandler,
19
- u as shouldFireEvent,
19
+ p as getHotkeyHandler,
20
+ f as shouldFireEvent,
20
21
  r as useClickOutside,
21
- p as useHotkeys,
22
- i as useInViewport,
23
- m as useInterval,
24
- n as useIsMounted,
25
- a as useLocalStorage,
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
- g as useResizeObserver,
28
- v as useUncontrolled,
29
- z as useUniqueId,
30
- S as useViewportSize,
31
- M as useVisualViewportSize
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.1",
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
- "peerDependencies": {
40
- "react": "^19.1.0",
41
- "react-dom": "^19.1.0"
42
- },
43
- "gitHead": "dcc216644c8c3e7d43a49ea655a22aed21fa4b83"
39
+ "gitHead": "e2e7b5c53c77d1773d7e0e463bac2a75b936a0d3"
44
40
  }