@vibehooks/react 0.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 (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +101 -0
  3. package/dist/index.d.ts +54 -0
  4. package/dist/index.js +55 -0
  5. package/dist/useAsyncState.d.ts +52 -0
  6. package/dist/useAsyncState.js +173 -0
  7. package/dist/useAudio.d.ts +26 -0
  8. package/dist/useAudio.js +64 -0
  9. package/dist/useAutoScroll.d.ts +47 -0
  10. package/dist/useAutoScroll.js +122 -0
  11. package/dist/useBarcode.d.ts +77 -0
  12. package/dist/useBarcode.js +140 -0
  13. package/dist/useBatteryStatus.d.ts +53 -0
  14. package/dist/useBatteryStatus.js +67 -0
  15. package/dist/useBodyScrollFreeze.d.ts +36 -0
  16. package/dist/useBodyScrollFreeze.js +74 -0
  17. package/dist/useCameraCapture.d.ts +76 -0
  18. package/dist/useCameraCapture.js +116 -0
  19. package/dist/useCookies.d.ts +42 -0
  20. package/dist/useCookies.js +61 -0
  21. package/dist/useCopyToClipboard.d.ts +22 -0
  22. package/dist/useCopyToClipboard.js +31 -0
  23. package/dist/useCountDown.d.ts +80 -0
  24. package/dist/useCountDown.js +106 -0
  25. package/dist/useDebouncedState.d.ts +47 -0
  26. package/dist/useDebouncedState.js +47 -0
  27. package/dist/useExternalNotifications.d.ts +36 -0
  28. package/dist/useExternalNotifications.js +100 -0
  29. package/dist/useFile.d.ts +74 -0
  30. package/dist/useFile.js +74 -0
  31. package/dist/useFullScreen.d.ts +20 -0
  32. package/dist/useFullScreen.js +43 -0
  33. package/dist/useGeolocation.d.ts +47 -0
  34. package/dist/useGeolocation.js +68 -0
  35. package/dist/useHoverIntent.d.ts +45 -0
  36. package/dist/useHoverIntent.js +81 -0
  37. package/dist/useIdle.d.ts +47 -0
  38. package/dist/useIdle.js +59 -0
  39. package/dist/useIndexedDB.d.ts +60 -0
  40. package/dist/useIndexedDB.js +75 -0
  41. package/dist/useIntersectionObserver.d.ts +45 -0
  42. package/dist/useIntersectionObserver.js +70 -0
  43. package/dist/useIntervalSafe.d.ts +72 -0
  44. package/dist/useIntervalSafe.js +85 -0
  45. package/dist/useIsClient.d.ts +12 -0
  46. package/dist/useIsClient.js +21 -0
  47. package/dist/useIsDesktop.d.ts +12 -0
  48. package/dist/useIsDesktop.js +23 -0
  49. package/dist/useIsFirstRender.d.ts +12 -0
  50. package/dist/useIsFirstRender.js +21 -0
  51. package/dist/useList.d.ts +19 -0
  52. package/dist/useList.js +44 -0
  53. package/dist/useLocalNotifications.d.ts +23 -0
  54. package/dist/useLocalNotifications.js +50 -0
  55. package/dist/useLocalStorage.d.ts +45 -0
  56. package/dist/useLocalStorage.js +71 -0
  57. package/dist/useNetworkInformation.d.ts +138 -0
  58. package/dist/useNetworkInformation.js +76 -0
  59. package/dist/useOnline.d.ts +17 -0
  60. package/dist/useOnline.js +29 -0
  61. package/dist/usePageVisibility.d.ts +32 -0
  62. package/dist/usePageVisibility.js +65 -0
  63. package/dist/usePermissions.d.ts +28 -0
  64. package/dist/usePermissions.js +70 -0
  65. package/dist/usePictureInPicture.d.ts +47 -0
  66. package/dist/usePictureInPicture.js +60 -0
  67. package/dist/usePopover.d.ts +54 -0
  68. package/dist/usePopover.js +67 -0
  69. package/dist/usePreferredLanguage.d.ts +55 -0
  70. package/dist/usePreferredLanguage.js +127 -0
  71. package/dist/usePreferredTheme.d.ts +67 -0
  72. package/dist/usePreferredTheme.js +133 -0
  73. package/dist/usePreviousDistinct.d.ts +12 -0
  74. package/dist/usePreviousDistinct.js +23 -0
  75. package/dist/useResettableState.d.ts +15 -0
  76. package/dist/useResettableState.js +25 -0
  77. package/dist/useScreenOrientation.d.ts +48 -0
  78. package/dist/useScreenOrientation.js +51 -0
  79. package/dist/useScreenSize.d.ts +16 -0
  80. package/dist/useScreenSize.js +34 -0
  81. package/dist/useScreenWakeLock.d.ts +37 -0
  82. package/dist/useScreenWakeLock.js +48 -0
  83. package/dist/useServerSentEvent.d.ts +57 -0
  84. package/dist/useServerSentEvent.js +78 -0
  85. package/dist/useShoppingCart.d.ts +54 -0
  86. package/dist/useShoppingCart.js +122 -0
  87. package/dist/useSmartVideo.d.ts +35 -0
  88. package/dist/useSmartVideo.js +76 -0
  89. package/dist/useSpeech.d.ts +74 -0
  90. package/dist/useSpeech.js +156 -0
  91. package/dist/useSummarizer.d.ts +92 -0
  92. package/dist/useSummarizer.js +83 -0
  93. package/dist/useTaskQueue.d.ts +25 -0
  94. package/dist/useTaskQueue.js +51 -0
  95. package/dist/useThrottledCallback.d.ts +32 -0
  96. package/dist/useThrottledCallback.js +42 -0
  97. package/dist/useTimeout.d.ts +58 -0
  98. package/dist/useTimeout.js +70 -0
  99. package/dist/useToggle.d.ts +30 -0
  100. package/dist/useToggle.js +23 -0
  101. package/dist/useTraceUpdates.d.ts +22 -0
  102. package/dist/useTraceUpdates.js +38 -0
  103. package/dist/useTranslator.d.ts +110 -0
  104. package/dist/useTranslator.js +119 -0
  105. package/dist/useUserActivation.d.ts +40 -0
  106. package/dist/useUserActivation.js +63 -0
  107. package/dist/useVibration.d.ts +55 -0
  108. package/dist/useVibration.js +50 -0
  109. package/dist/useWebsocket.d.ts +80 -0
  110. package/dist/useWebsocket.js +125 -0
  111. package/package.json +70 -0
@@ -0,0 +1,57 @@
1
+ //#region src/useServerSentEvent.d.ts
2
+ interface UseServerSentEventOptions {
3
+ /**
4
+ * Whether the connection should be established.
5
+ * Usefull to lazily connect.
6
+ */
7
+ enabled?: boolean;
8
+ /**
9
+ * Called when an error occurs.
10
+ */
11
+ onError?: (event: Event) => void;
12
+ /**
13
+ * Called on every incoming message event.
14
+ */
15
+ onMessage?: (event: MessageEvent<string>) => void;
16
+ /**
17
+ * Called then the connection is opened.
18
+ */
19
+ onOpen?: (event: Event) => void;
20
+ /**
21
+ * Whether credentials (cookies) should be sent.
22
+ */
23
+ withCredentials?: boolean;
24
+ }
25
+ interface UseServerSentEventReturn {
26
+ /**
27
+ * Manually closes the connection.
28
+ */
29
+ close: () => void;
30
+ /**
31
+ * Current connection state.
32
+ */
33
+ readyState: SSEReadyState | null;
34
+ }
35
+ type SSEReadyState = 0 | 1 | 2;
36
+ /**
37
+ * 0 = CONNECTING
38
+ * 1 = OPEN
39
+ * 2 = CLOSED
40
+ */
41
+ /**
42
+ * `useServerSideEvent` provides unopinionated access to Server-Sent Event (SSE) via the EventSource Web API.
43
+ * It manages the EventSource lifecycle but delegates data handling to consumer callbacks.
44
+ *
45
+ * @example
46
+ * ```tsx
47
+ * useServerSideEvent('/api/events', {
48
+ * onMessage: (e) => {
49
+ * const data = JSON.parse(e.data);
50
+ * console.log(data);
51
+ * }
52
+ * });
53
+ * ```
54
+ */
55
+ declare function useServerSentEvent(url: string, options?: UseServerSentEventOptions): UseServerSentEventReturn;
56
+ //#endregion
57
+ export { useServerSentEvent };
@@ -0,0 +1,78 @@
1
+ import * as React from "react";
2
+
3
+ //#region src/useServerSentEvent.ts
4
+ /**
5
+ * 0 = CONNECTING
6
+ * 1 = OPEN
7
+ * 2 = CLOSED
8
+ */
9
+ /**
10
+ * `useServerSideEvent` provides unopinionated access to Server-Sent Event (SSE) via the EventSource Web API.
11
+ * It manages the EventSource lifecycle but delegates data handling to consumer callbacks.
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * useServerSideEvent('/api/events', {
16
+ * onMessage: (e) => {
17
+ * const data = JSON.parse(e.data);
18
+ * console.log(data);
19
+ * }
20
+ * });
21
+ * ```
22
+ */
23
+ function useServerSentEvent(url, options = {}) {
24
+ const { enabled = true, onError, onMessage, onOpen, withCredentials = false } = options;
25
+ const sourceRef = React.useRef(null);
26
+ const [readyState, setReadyState] = React.useState(null);
27
+ const onMessageRef = React.useRef(onMessage);
28
+ const onOpenRef = React.useRef(onOpen);
29
+ const onErrorRef = React.useRef(onError);
30
+ const isSupported = typeof window !== "undefined" && typeof EventSource !== "undefined";
31
+ const close = React.useCallback(() => {
32
+ sourceRef.current?.close();
33
+ sourceRef.current = null;
34
+ setReadyState(null);
35
+ }, []);
36
+ React.useEffect(() => {
37
+ onMessageRef.current = onMessage;
38
+ onOpenRef.current = onOpen;
39
+ onErrorRef.current = onError;
40
+ }, [
41
+ onMessageRef,
42
+ onOpenRef,
43
+ onErrorRef
44
+ ]);
45
+ React.useEffect(() => {
46
+ if (!isSupported || !enabled) return;
47
+ const source = new EventSource(url, { withCredentials });
48
+ sourceRef.current = source;
49
+ source.onopen = (event) => {
50
+ setReadyState(source.readyState);
51
+ onOpenRef.current?.(event);
52
+ };
53
+ source.onmessage = (event) => {
54
+ setReadyState(source.readyState);
55
+ onMessageRef.current?.(event);
56
+ };
57
+ source.onerror = (event) => {
58
+ setReadyState(source.readyState);
59
+ onErrorRef.current?.(event);
60
+ };
61
+ return () => {
62
+ source.close();
63
+ sourceRef.current = null;
64
+ };
65
+ }, [
66
+ url,
67
+ enabled,
68
+ withCredentials,
69
+ isSupported
70
+ ]);
71
+ return {
72
+ close,
73
+ readyState
74
+ };
75
+ }
76
+
77
+ //#endregion
78
+ export { useServerSentEvent };
@@ -0,0 +1,54 @@
1
+ //#region src/useShoppingCart.d.ts
2
+ interface UseShoppingCartOptions<T extends object> {
3
+ getItemDiscount?: (item: T) => number;
4
+ getItemKey: (item: T) => string | number;
5
+ getItemPrice: (item: T) => number;
6
+ getItemQuantity: (item: T) => number;
7
+ getItemTax?: (item: T) => number;
8
+ }
9
+ interface ShoppingCartItemDetail {
10
+ discount: number;
11
+ key: string | number;
12
+ quantity: number;
13
+ subtotal: number;
14
+ tax: number;
15
+ total: number;
16
+ unitPrice: number;
17
+ }
18
+ interface UseShoppingCartReturn<T extends object> {
19
+ addItem: (item: T) => void;
20
+ clear: () => void;
21
+ getDetails: () => ShoppingCartItemDetail[];
22
+ getItemCount: () => number;
23
+ getSubtotal: () => number;
24
+ getTotal: () => number;
25
+ getTotalDiscount: () => number;
26
+ getTotalQuantity: () => number;
27
+ getTotalTax: () => number;
28
+ items: T[];
29
+ removeItem: (id: number | string) => void;
30
+ updateItem: (id: number | string, patch: Partial<T>) => void;
31
+ }
32
+ /**
33
+ * `useShoppingCart` is a fully unopinionated React hook for managing shopping cart state.
34
+ * The hook does not assume any data structure; instead, it relies on user-provided extractor functions to derive semantic meaning (identity, price, quantity, taxes, discounts).
35
+ * This design allows the hook to adapt to any domain model while still providing a complete and ergonomic shopping cart API.
36
+ *
37
+ * @example
38
+ * ```tsx
39
+ * const cart = useShoppingCart<Product>({
40
+ * getItemKey: p => p.id,
41
+ * getItemPrice: p => p.price,
42
+ * getItemQuantity: p => p.quantity,
43
+ * getItemTax: p => p.tax ?? 0,
44
+ * getItemDiscount: p => p.discount ?? 0,
45
+ * });
46
+ *
47
+ * cart.addItem(product);
48
+ * cart.getTotal();
49
+ * cart.getDetails();
50
+ * ```
51
+ */
52
+ declare function useShoppingCart<T extends object>(options: UseShoppingCartOptions<T>): UseShoppingCartReturn<T>;
53
+ //#endregion
54
+ export { useShoppingCart };
@@ -0,0 +1,122 @@
1
+ import * as React from "react";
2
+
3
+ //#region src/useShoppingCart.ts
4
+ /**
5
+ * `useShoppingCart` is a fully unopinionated React hook for managing shopping cart state.
6
+ * The hook does not assume any data structure; instead, it relies on user-provided extractor functions to derive semantic meaning (identity, price, quantity, taxes, discounts).
7
+ * This design allows the hook to adapt to any domain model while still providing a complete and ergonomic shopping cart API.
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * const cart = useShoppingCart<Product>({
12
+ * getItemKey: p => p.id,
13
+ * getItemPrice: p => p.price,
14
+ * getItemQuantity: p => p.quantity,
15
+ * getItemTax: p => p.tax ?? 0,
16
+ * getItemDiscount: p => p.discount ?? 0,
17
+ * });
18
+ *
19
+ * cart.addItem(product);
20
+ * cart.getTotal();
21
+ * cart.getDetails();
22
+ * ```
23
+ */
24
+ function useShoppingCart(options) {
25
+ const { getItemDiscount, getItemKey, getItemPrice, getItemQuantity, getItemTax } = options;
26
+ const [items, setItems] = React.useState([]);
27
+ const addItem = React.useCallback((item) => {
28
+ setItems((prevItems) => [...prevItems, item]);
29
+ }, []);
30
+ const removeItem = React.useCallback((key) => {
31
+ setItems((prevItems) => prevItems.filter((item) => getItemKey(item) !== key));
32
+ }, [getItemKey]);
33
+ const updateItem = React.useCallback((key, patch) => {
34
+ setItems((prev) => prev.map((item) => getItemKey(item) === key ? {
35
+ ...item,
36
+ ...patch
37
+ } : item));
38
+ }, [getItemKey]);
39
+ const clear = React.useCallback(() => {
40
+ setItems([]);
41
+ }, []);
42
+ const getItemCount = React.useCallback(() => items.length, [items]);
43
+ const getTotalQuantity = React.useCallback(() => items.reduce((acc, item) => acc + getItemQuantity(item), 0), [items, getItemQuantity]);
44
+ const getSubtotal = React.useCallback(() => items.reduce((acc, item) => acc + getItemPrice(item) * getItemQuantity(item), 0), [
45
+ items,
46
+ getItemPrice,
47
+ getItemQuantity
48
+ ]);
49
+ const getTotalTax = React.useCallback(() => getItemTax ? items.reduce((acc, item) => acc + getItemTax(item) * getItemQuantity(item), 0) : 0, [
50
+ items,
51
+ getItemTax,
52
+ getItemQuantity
53
+ ]);
54
+ const getTotalDiscount = React.useCallback(() => getItemDiscount ? items.reduce((acc, item) => acc + getItemDiscount(item) * getItemQuantity(item), 0) : 0, [
55
+ items,
56
+ getItemDiscount,
57
+ getItemQuantity
58
+ ]);
59
+ const getTotal = React.useCallback(() => getSubtotal() + getTotalTax() + getTotalDiscount(), [
60
+ getSubtotal,
61
+ getTotalTax,
62
+ getTotalDiscount
63
+ ]);
64
+ return {
65
+ addItem,
66
+ clear,
67
+ getDetails: React.useCallback(() => {
68
+ const map = /* @__PURE__ */ new Map();
69
+ for (const item of items) {
70
+ const key = getItemKey(item);
71
+ const quantity = getItemQuantity(item);
72
+ const unitPrice = getItemPrice(item);
73
+ const tax = getItemTax ? getItemTax(item) : 0;
74
+ const discount = getItemDiscount ? getItemDiscount(item) : 0;
75
+ const existing = map.get(key);
76
+ if (!existing) {
77
+ const subtotal = quantity * unitPrice;
78
+ const taxTotal = tax * quantity;
79
+ const discountTotal = discount * quantity;
80
+ map.set(key, {
81
+ discount: discountTotal,
82
+ key,
83
+ quantity,
84
+ subtotal,
85
+ tax: taxTotal,
86
+ total: subtotal + taxTotal - discountTotal,
87
+ unitPrice
88
+ });
89
+ } else {
90
+ existing.quantity += quantity;
91
+ const subtTotalIncrement = quantity * unitPrice;
92
+ const taxIncrement = tax * quantity;
93
+ const discountIncrement = discount * quantity;
94
+ existing.subtotal += subtTotalIncrement;
95
+ existing.tax += taxIncrement;
96
+ existing.discount += discountIncrement;
97
+ existing.total += subtTotalIncrement + taxIncrement - discountIncrement;
98
+ }
99
+ }
100
+ return Array.from(map.values());
101
+ }, [
102
+ items,
103
+ getItemKey,
104
+ getItemPrice,
105
+ getItemQuantity,
106
+ getItemTax,
107
+ getItemDiscount
108
+ ]),
109
+ getItemCount,
110
+ getSubtotal,
111
+ getTotal,
112
+ getTotalDiscount,
113
+ getTotalQuantity,
114
+ getTotalTax,
115
+ items,
116
+ removeItem,
117
+ updateItem
118
+ };
119
+ }
120
+
121
+ //#endregion
122
+ export { useShoppingCart };
@@ -0,0 +1,35 @@
1
+ import * as React from "react";
2
+
3
+ //#region src/useSmartVideo.d.ts
4
+ interface SmartVideoOptions {
5
+ /**
6
+ * Auto-play when video becomes visible.
7
+ */
8
+ autoPlay?: boolean;
9
+ /**
10
+ * Pause video when it leaves the viewport.
11
+ */
12
+ pauseOnExit?: boolean;
13
+ /**
14
+ * Reset video to time 0 when it leaves the viewport.
15
+ */
16
+ resetOnExit?: boolean;
17
+ /**
18
+ * Percentage of visibility required to start playing.
19
+ * (0.0 - 1.0)
20
+ * Default: 0.5
21
+ */
22
+ threshold?: number;
23
+ }
24
+ interface SmartVideoReturn {
25
+ isPlaying: boolean;
26
+ isVisible: boolean;
27
+ pause: () => void;
28
+ play: () => Promise<void>;
29
+ reset: () => void;
30
+ stop: () => void;
31
+ videoRef: React.RefObject<HTMLVideoElement | null>;
32
+ }
33
+ declare function useSmartVideo(options?: SmartVideoOptions): SmartVideoReturn;
34
+ //#endregion
35
+ export { useSmartVideo };
@@ -0,0 +1,76 @@
1
+ import { useIntersectionObserver } from "./useIntersectionObserver.js";
2
+ import * as React from "react";
3
+
4
+ //#region src/useSmartVideo.ts
5
+ function useSmartVideo(options = {}) {
6
+ const { autoPlay, pauseOnExit, resetOnExit, threshold } = options;
7
+ const videoRef = React.useRef(null);
8
+ const { isVisible } = useIntersectionObserver({ threshold: threshold ?? .5 }, videoRef);
9
+ const [isPlaying, setIsPlaying] = React.useState(false);
10
+ React.useEffect(() => {
11
+ const video = videoRef.current;
12
+ if (!video) return;
13
+ const canPlay = canAutoPlay(video);
14
+ if (isVisible) {
15
+ if (autoPlay && canPlay && video.paused) video.play().catch(() => {});
16
+ } else {
17
+ if (pauseOnExit && !video.paused) video.pause();
18
+ if (resetOnExit) video.currentTime = 0;
19
+ }
20
+ }, [
21
+ isVisible,
22
+ autoPlay,
23
+ pauseOnExit,
24
+ resetOnExit
25
+ ]);
26
+ React.useEffect(() => {
27
+ const video = videoRef.current;
28
+ if (!video) return;
29
+ const onPlay = () => setIsPlaying(true);
30
+ const onPause = () => setIsPlaying(false);
31
+ video.addEventListener("play", onPlay);
32
+ video.addEventListener("pause", onPause);
33
+ return () => {
34
+ video.removeEventListener("play", onPlay);
35
+ video.removeEventListener("pause", onPause);
36
+ };
37
+ }, []);
38
+ const play = React.useCallback(async () => {
39
+ if (!videoRef.current) return;
40
+ await safePlay(videoRef.current);
41
+ }, []);
42
+ const pause = React.useCallback(() => {
43
+ videoRef.current?.pause();
44
+ }, []);
45
+ const stop = React.useCallback(() => {
46
+ const video = videoRef.current;
47
+ if (!video) return;
48
+ video.pause();
49
+ video.currentTime = 0;
50
+ }, []);
51
+ return {
52
+ isPlaying,
53
+ isVisible,
54
+ pause,
55
+ play,
56
+ reset: React.useCallback(() => {
57
+ if (videoRef.current) videoRef.current.currentTime = 0;
58
+ }, []),
59
+ stop,
60
+ videoRef
61
+ };
62
+ }
63
+ async function safePlay(video) {
64
+ try {
65
+ await video.play();
66
+ return true;
67
+ } catch {
68
+ return false;
69
+ }
70
+ }
71
+ function canAutoPlay(video) {
72
+ return video.muted && video.playsInline;
73
+ }
74
+
75
+ //#endregion
76
+ export { useSmartVideo };
@@ -0,0 +1,74 @@
1
+ //#region src/useSpeech.d.ts
2
+ interface SpeechRecognition extends EventTarget {
3
+ abort(): void;
4
+ continuous: boolean;
5
+ interimResults: boolean;
6
+ lang: string;
7
+ onend: ((this: SpeechRecognition, ev: Event) => unknown) | null;
8
+ onerror: ((this: SpeechRecognition, ev: SpeechRecognitionErrorEvent) => unknown) | null;
9
+ onresult: ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => unknown) | null;
10
+ start(): void;
11
+ stop(): void;
12
+ }
13
+ interface SpeechRecognitionEvent extends Event {
14
+ readonly resultIndex: number;
15
+ readonly results: SpeechRecognitionResultList;
16
+ }
17
+ interface SpeechRecognitionResultList {
18
+ [index: number]: SpeechRecognitionResult;
19
+ item(index: number): SpeechRecognitionResult;
20
+ readonly length: number;
21
+ }
22
+ interface SpeechRecognitionResult {
23
+ [index: number]: SpeechRecognitionAlternative;
24
+ readonly isFinal: boolean;
25
+ item(index: number): SpeechRecognitionAlternative;
26
+ readonly length: number;
27
+ }
28
+ interface SpeechRecognitionAlternative {
29
+ readonly confidence: number;
30
+ readonly transcript: string;
31
+ }
32
+ interface SpeechRecognitionErrorEvent extends Event {
33
+ readonly error: string;
34
+ readonly message?: string;
35
+ }
36
+ interface SpeechRecognitionConstructor {
37
+ new (): SpeechRecognition;
38
+ }
39
+ declare global {
40
+ interface Window {
41
+ SpeechRecognition?: SpeechRecognitionConstructor;
42
+ webkitSpeechRecognition?: SpeechRecognitionConstructor;
43
+ }
44
+ }
45
+ type SpeechStatus = 'idle' | 'listening' | 'stopped' | 'unsupported' | 'error';
46
+ interface UseSpeechOptions {
47
+ continuous?: boolean;
48
+ interimResults?: boolean;
49
+ lang?: string;
50
+ }
51
+ interface UseSpeechReturn {
52
+ error: Error | null;
53
+ finalTranscript: string;
54
+ interimTranscript: string;
55
+ reset: () => void;
56
+ start: () => void;
57
+ status: SpeechStatus;
58
+ stop: () => void;
59
+ transcript: string;
60
+ }
61
+ /**
62
+ * `useSpeech` is a React hook that provides unopinionated access to the Speech API.
63
+ *
64
+ * @example
65
+ * ```tsx
66
+ * const speech = useSpeech({ lang: 'en-US' });
67
+ * speech.start();
68
+ * ```
69
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API
70
+ *
71
+ */
72
+ declare function useSpeech(options?: UseSpeechOptions): UseSpeechReturn;
73
+ //#endregion
74
+ export { useSpeech };
@@ -0,0 +1,156 @@
1
+ import * as React from "react";
2
+
3
+ //#region src/useSpeech.ts
4
+ function isChrome() {
5
+ if (typeof navigator === "undefined") return false;
6
+ const ua = navigator.userAgent;
7
+ const isChromium = ua.includes("Chrome");
8
+ const isEdge = ua.includes("Edg");
9
+ const isBrave = typeof navigator.brave === "object";
10
+ return isChromium && !isEdge && !isBrave;
11
+ }
12
+ /**
13
+ * `useSpeech` is a React hook that provides unopinionated access to the Speech API.
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * const speech = useSpeech({ lang: 'en-US' });
18
+ * speech.start();
19
+ * ```
20
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API
21
+ *
22
+ */
23
+ function useSpeech(options = {}) {
24
+ const isClient = typeof window !== "undefined";
25
+ const isChromeBrowser = React.useMemo(() => isChrome(), []);
26
+ const recognitionRef = React.useRef(null);
27
+ const isManuallyStoppedRef = React.useRef(false);
28
+ const shouldRestartRef = React.useRef(false);
29
+ const nextStatusRef = React.useRef(null);
30
+ const [status, setStatus] = React.useState("idle");
31
+ const [error, setError] = React.useState(null);
32
+ const [finalTranscript, setFinalTranscript] = React.useState("");
33
+ const [interimTranscript, setInterimTranscript] = React.useState("");
34
+ const initializeRecognition = React.useCallback(() => {
35
+ if (!isClient) return;
36
+ if (!isChromeBrowser) {
37
+ setStatus("unsupported");
38
+ return;
39
+ }
40
+ const SpeechRecognitionCtor = window.SpeechRecognition ?? window.webkitSpeechRecognition;
41
+ if (!SpeechRecognitionCtor) {
42
+ setStatus("unsupported");
43
+ return;
44
+ }
45
+ const recognition = new SpeechRecognitionCtor();
46
+ recognition.lang = options.lang ?? "en-US";
47
+ recognition.continuous = options.continuous ?? true;
48
+ recognition.interimResults = options.interimResults ?? true;
49
+ recognition.onresult = (event) => {
50
+ let interim = "";
51
+ let final = "";
52
+ for (let i = event.resultIndex; i < event.results.length; i++) {
53
+ const result = event.results[i];
54
+ const text = result?.[0]?.transcript ?? "";
55
+ if (result?.isFinal) final += text + " ";
56
+ else interim += text;
57
+ }
58
+ if (interim) setInterimTranscript(interim);
59
+ if (final) {
60
+ setFinalTranscript((prev) => prev + final);
61
+ setInterimTranscript("");
62
+ }
63
+ };
64
+ recognition.onerror = (event) => {
65
+ if (isManuallyStoppedRef.current) return;
66
+ if (event.error === "no-speech") {
67
+ console.warn("No se detectó voz.");
68
+ shouldRestartRef.current = true;
69
+ return;
70
+ }
71
+ if (event.error === "aborted") return;
72
+ setError(new Error(event.error));
73
+ setStatus("error");
74
+ };
75
+ recognition.onend = () => {
76
+ if (isManuallyStoppedRef.current) {
77
+ isManuallyStoppedRef.current = false;
78
+ setStatus(nextStatusRef.current ?? "stopped");
79
+ return;
80
+ }
81
+ if (status === "error") return;
82
+ if (shouldRestartRef.current) {
83
+ try {
84
+ recognition.start();
85
+ setStatus("listening");
86
+ } catch (err) {
87
+ console.error("Error al reiniciar reconocimiento:", err);
88
+ setError(err instanceof Error ? err : /* @__PURE__ */ new Error("Failed to restart"));
89
+ setStatus("error");
90
+ shouldRestartRef.current = false;
91
+ }
92
+ return;
93
+ }
94
+ setStatus("stopped");
95
+ };
96
+ recognitionRef.current = recognition;
97
+ }, [
98
+ isClient,
99
+ options.lang,
100
+ options.continuous,
101
+ options.interimResults
102
+ ]);
103
+ const start = React.useCallback(() => {
104
+ if (!recognitionRef.current) initializeRecognition();
105
+ const recognition = recognitionRef.current;
106
+ if (!recognition) return;
107
+ try {
108
+ isManuallyStoppedRef.current = false;
109
+ shouldRestartRef.current = true;
110
+ recognition.start();
111
+ setStatus("listening");
112
+ setError(null);
113
+ } catch (err) {
114
+ console.error("Error al iniciar reconocimiento:", err);
115
+ setError(err instanceof Error ? err : /* @__PURE__ */ new Error("Failed to start"));
116
+ setStatus("error");
117
+ }
118
+ }, [initializeRecognition]);
119
+ const stop = React.useCallback(() => {
120
+ isManuallyStoppedRef.current = true;
121
+ shouldRestartRef.current = false;
122
+ if (recognitionRef.current) recognitionRef.current.stop();
123
+ }, []);
124
+ const reset = React.useCallback(() => {
125
+ nextStatusRef.current = "idle";
126
+ stop();
127
+ setFinalTranscript("");
128
+ setInterimTranscript("");
129
+ setError(null);
130
+ }, [stop]);
131
+ const transcript = React.useMemo(() => `${finalTranscript}${interimTranscript}`.trim(), [finalTranscript, interimTranscript]);
132
+ React.useEffect(() => {
133
+ initializeRecognition();
134
+ return () => {
135
+ isManuallyStoppedRef.current = true;
136
+ shouldRestartRef.current = false;
137
+ if (recognitionRef.current) {
138
+ recognitionRef.current.stop();
139
+ recognitionRef.current = null;
140
+ }
141
+ };
142
+ }, [initializeRecognition]);
143
+ return {
144
+ error,
145
+ finalTranscript: finalTranscript.trim(),
146
+ interimTranscript,
147
+ reset,
148
+ start,
149
+ status,
150
+ stop,
151
+ transcript
152
+ };
153
+ }
154
+
155
+ //#endregion
156
+ export { useSpeech };