@untemps/react-vocal 2.0.0-beta.1 → 2.0.0-beta.11

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 (43) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/README.md +55 -21
  3. package/dist/index.cjs +7 -0
  4. package/dist/index.es.js +330 -1914
  5. package/package.json +25 -9
  6. package/.github/workflows/publish.yml +0 -32
  7. package/.husky/commit-msg +0 -1
  8. package/.husky/pre-commit +0 -1
  9. package/.prettierignore +0 -3
  10. package/.prettierrc +0 -29
  11. package/CLAUDE.md +0 -59
  12. package/assets/icon-idle.png +0 -0
  13. package/assets/icon-listening.png +0 -0
  14. package/assets/microphone.png +0 -0
  15. package/assets/react-vocal.png +0 -0
  16. package/commitlint.config.js +0 -7
  17. package/dev/index.html +0 -24
  18. package/dev/package.json +0 -18
  19. package/dev/public/index.html +0 -24
  20. package/dev/src/index.jsx +0 -45
  21. package/dev/vite.config.js +0 -10
  22. package/dev/yarn.lock +0 -201
  23. package/dist/index.es.js.map +0 -1
  24. package/dist/index.js +0 -9
  25. package/dist/index.js.map +0 -1
  26. package/dist/index.umd.js +0 -9
  27. package/dist/index.umd.js.map +0 -1
  28. package/src/components/Icon.jsx +0 -24
  29. package/src/components/Vocal.jsx +0 -168
  30. package/src/components/__tests__/Icon.test.jsx +0 -38
  31. package/src/components/__tests__/Vocal.test.jsx +0 -270
  32. package/src/components/__tests__/VocalWithMockedUseVocal.test.jsx +0 -38
  33. package/src/components/__tests__/__snapshots__/Icon.test.jsx.snap +0 -21
  34. package/src/components/__tests__/__snapshots__/Vocal.test.jsx.snap +0 -28
  35. package/src/hooks/__tests__/useCommands.test.js +0 -64
  36. package/src/hooks/__tests__/useTimeout.test.js +0 -69
  37. package/src/hooks/__tests__/useVocal.test.js +0 -197
  38. package/src/hooks/useCommands.js +0 -21
  39. package/src/hooks/useTimeout.js +0 -21
  40. package/src/hooks/useVocal.js +0 -56
  41. package/src/index.js +0 -7
  42. package/vite.config.js +0 -35
  43. package/vitest.setup.js +0 -71
package/CHANGELOG.md CHANGED
@@ -1,3 +1,94 @@
1
+ # [2.0.0-beta.11](https://github.com/untemps/react-vocal/compare/v2.0.0-beta.10...v2.0.0-beta.11) (2026-05-24)
2
+
3
+
4
+ ### chore
5
+
6
+ * Align package.json with @untemps/vocal 2.x packaging shape ([#150](https://github.com/untemps/react-vocal/issues/150)) ([7701738](https://github.com/untemps/react-vocal/commit/770173880c3cfa32b6e572bd775b35333fbe0531))
7
+
8
+
9
+ ### BREAKING CHANGES
10
+
11
+ * the CJS bundle is now published as `dist/index.cjs` instead of `dist/index.js`. Consumers using the standard package entry point (`require('@untemps/react-vocal')`) are not affected — the `main`/`exports.require` fields resolve to the new path automatically.
12
+ Consumers that hard-coded a deep require to `@untemps/react-vocal/dist/index.js` must update the path to `@untemps/react-vocal/dist/index.cjs`, and bundlers that respect the `exports` map will reject sub-path imports outright — switch to the package entry import.
13
+
14
+ # [2.0.0-beta.10](https://github.com/untemps/react-vocal/compare/v2.0.0-beta.9...v2.0.0-beta.10) (2026-05-24)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * Add aria-hidden to decorative SVG in Icon component ([#148](https://github.com/untemps/react-vocal/issues/148)) ([5e00e34](https://github.com/untemps/react-vocal/commit/5e00e34e991b1a5c1a29a393ac885a76864c601c))
20
+
21
+ # [2.0.0-beta.9](https://github.com/untemps/react-vocal/compare/v2.0.0-beta.8...v2.0.0-beta.9) (2026-05-24)
22
+
23
+
24
+ ### Bug Fixes
25
+
26
+ * Prevent stale Fuse instance in useCommands after async fuse.js import ([#147](https://github.com/untemps/react-vocal/issues/147)) ([a83f06d](https://github.com/untemps/react-vocal/commit/a83f06d73644d1029e735b38dc06c8614470e19e))
27
+
28
+ # [2.0.0-beta.8](https://github.com/untemps/react-vocal/compare/v2.0.0-beta.7...v2.0.0-beta.8) (2026-05-24)
29
+
30
+
31
+ ### Features
32
+
33
+ * Migrate to @untemps/vocal 2.x functional API ([#146](https://github.com/untemps/react-vocal/issues/146)) ([582575f](https://github.com/untemps/react-vocal/commit/582575f391472e0e95b0c793b57c7c3edca71939))
34
+
35
+
36
+ ### BREAKING CHANGES
37
+
38
+ * `isSupported` is now a function instead of a boolean snapshot. Consumers must call it as `isSupported()`.
39
+ Migration:
40
+ // before
41
+ import { isSupported } from '@untemps/react-vocal'
42
+ if (isSupported) { ... }
43
+ // after
44
+ import { isSupported } from '@untemps/react-vocal'
45
+ if (isSupported()) { ... }
46
+ The function form is SSR-safe — it returns `false` when `window` is undefined, so consumers no longer need a manual `typeof window` guard before checking support.
47
+ * UMD bundle is no longer published.
48
+ `dist/index.umd.js` is removed from the release artifacts and from the npm tarball. Consumers loading react-vocal via `<script src="…/dist/index.umd.js">` or an AMD loader must migrate to the ES bundle (`dist/index.es.js`) via a module-aware loader (or use a bundler). The CJS bundle (`dist/index.js`) remains available for Node-style require().
49
+
50
+ # [2.0.0-beta.7](https://github.com/untemps/react-vocal/compare/v2.0.0-beta.6...v2.0.0-beta.7) (2026-05-13)
51
+
52
+ # [2.0.0-beta.6](https://github.com/untemps/react-vocal/compare/v2.0.0-beta.5...v2.0.0-beta.6) (2026-05-12)
53
+
54
+
55
+ ### Features
56
+
57
+ * Add continuous session support ([#118](https://github.com/untemps/react-vocal/issues/118)) ([690ba61](https://github.com/untemps/react-vocal/commit/690ba617746aa28499d7ea2d48751453a652ff5e))
58
+
59
+ # [2.0.0-beta.5](https://github.com/untemps/react-vocal/compare/v2.0.0-beta.4...v2.0.0-beta.5) (2026-05-11)
60
+
61
+
62
+ ### Features
63
+
64
+ * Make fuse.js an optional peer dependency ([#117](https://github.com/untemps/react-vocal/issues/117)) ([a1c2a33](https://github.com/untemps/react-vocal/commit/a1c2a337f8d4ba4729c81f0a9b8664bcf914755f))
65
+
66
+
67
+ ### BREAKING CHANGES
68
+
69
+ * fuse.js must now be installed separately to enable fuzzy matching for phrase commands.
70
+
71
+ # [2.0.0-beta.4](https://github.com/untemps/react-vocal/compare/v2.0.0-beta.3...v2.0.0-beta.4) (2026-05-10)
72
+
73
+
74
+ ### Features
75
+
76
+ * Per-segment command matching, maxAlternatives, and exact lookup ([39b6370](https://github.com/untemps/react-vocal/commit/39b63701b2a15c058b5ae510be8ad4b4437d6763))
77
+
78
+ # [2.0.0-beta.3](https://github.com/untemps/react-vocal/compare/v2.0.0-beta.2...v2.0.0-beta.3) (2026-05-07)
79
+
80
+
81
+ ### Features
82
+
83
+ * Aggregate all result segments picking highest-confidence alternative ([#113](https://github.com/untemps/react-vocal/issues/113)) ([600600d](https://github.com/untemps/react-vocal/commit/600600dfde457df6fe93974d76d0b48599846082))
84
+
85
+ # [2.0.0-beta.2](https://github.com/untemps/react-vocal/compare/v2.0.0-beta.1...v2.0.0-beta.2) (2026-05-07)
86
+
87
+
88
+ ### Bug Fixes
89
+
90
+ * Stabilize event handlers and fix stale prop closures ([#112](https://github.com/untemps/react-vocal/issues/112)) ([69c5c00](https://github.com/untemps/react-vocal/commit/69c5c00ed73e792b6dd91dd6b294163e1c3d00dc))
91
+
1
92
  # [2.0.0-beta.1](https://github.com/untemps/react-vocal/compare/v1.7.35...v2.0.0-beta.1) (2026-05-05)
2
93
 
3
94
 
package/README.md CHANGED
@@ -33,7 +33,7 @@ timeout elapses.
33
33
  Some browsers supports the `SpeechRecognition` API but not all the related APIs.
34
34
  For example, browsers on iOS 14.5, the `SpeechGrammar` and `SpeechGrammarList` and `Permissions` APIs are not supported.
35
35
 
36
- Although the lack of `SpeechGrammar` and `SpeechGrammarList` is handled by the underlaying `@untemps/vocal` library, you need to deal with `Permissions` by yourself.
36
+ Although the lack of `SpeechGrammar` and `SpeechGrammarList` is handled by the underlying `@untemps/vocal` library, you need to deal with `Permissions` by yourself.
37
37
 
38
38
  ## Requirements
39
39
 
@@ -46,6 +46,14 @@ Although the lack of `SpeechGrammar` and `SpeechGrammarList` is handled by the u
46
46
  yarn add @untemps/react-vocal
47
47
  ```
48
48
 
49
+ Fuzzy matching for phrase commands requires [fuse.js](https://fusejs.io/) as an optional peer dependency:
50
+
51
+ ```bash
52
+ yarn add fuse.js
53
+ ```
54
+
55
+ Without fuse.js, phrase commands fall back to case-insensitive exact matching. Single-word commands always use exact matching and never require fuse.js.
56
+
49
57
  ## Usage
50
58
 
51
59
  ### `Vocal` component
@@ -192,15 +200,17 @@ const commands = {
192
200
 
193
201
  The component utilizes a special hook called `useCommands` to respond to the commands.
194
202
  The hook performs a fuzzy search to match approximate commands if needed. This allows to fix accidental typos or approximate recognition results.
195
- To do so the hook uses [fuse.js](https://fusejs.io/) which implements an algorithm to find strings that are approximately equal to a given input. The score precision that distinguishes acceptable command-to-callback mapping from negative matching can be customized in the hook instantiantion.
203
+ To do so the hook uses [fuse.js](https://fusejs.io/) which implements an algorithm to find strings that are approximately equal to a given input. The score precision that distinguishes acceptable command-to-callback mapping from negative matching can be customized in the hook instantiation.
196
204
 
197
- ```javascript
198
- useCommands(commands, threshold) // threshold is the limit not to exceed to be considered a match
199
- ```
205
+ fuse.js is an optional peer dependency — install it separately to enable fuzzy matching (see [Installation](#installation)). Without it, phrase commands fall back to case-insensitive exact matching.
206
+
207
+ **Single-word command keys** (e.g. `rouge`, `submit`) use exact case-insensitive lookup. When the recognition returns a multi-word transcript, each word is tried individually so a command fires even when embedded in a phrase (e.g. _"je veux du rouge"_ triggers `rouge`).
208
+
209
+ **Phrase command keys** (e.g. `'Change the background color'`) use [fuse.js](https://fusejs.io/) fuzzy matching. The `precision` prop controls the Fuse.js score threshold (default `0.4` — lower is stricter).
200
210
 
201
- See [fuze.js scoring theory](https://fusejs.io/concepts/scoring-theory.html) for more details.
211
+ **Homophone tolerance** is achieved via `maxAlternatives`: by setting it to 3–5, the speech engine returns several transcription candidates per segment. The correct word (e.g. _vert_) may appear as a secondary alternative when the primary is a homophone (e.g. _verre_), and will still trigger the command.
202
212
 
203
- > :warning: **The `Vocal` component doesn't expose that score yet.** For now on you have to deal with the default value (*0.4*)
213
+ **At most one command fires per utterance.** Alternatives and segments are scanned in order and matching stops at the first hit, so a single recognition event can trigger at most one command callback.
204
214
 
205
215
  ---
206
216
 
@@ -208,10 +218,14 @@ See [fuze.js scoring theory](https://fusejs.io/concepts/scoring-theory.html) for
208
218
 
209
219
  | Props | Type | Default | Description |
210
220
  | ------------- | ----------------- | -------------------- | ----------------------------------------------------------------------------------------------- |
211
- | commands | object | null | Callbacks to be triggered when specified commands are detected by the recognition |
212
- | lang | string | 'en-US' | Language understood by the recognition [BCP 47 language tag](https://tools.ietf.org/html/bcp47) |
213
- | grammars | SpeechGrammarList | null | Grammars understood by the recognition [JSpeech Grammar Format](https://www.w3.org/TR/jsgf/) |
214
- | timeout | number | 3000 | Time in ms to wait before discarding the recognition |
221
+ | commands | object | null | Callbacks to be triggered when specified commands are detected by the recognition |
222
+ | lang | string | 'en-US' | Language understood by the recognition [BCP 47 language tag](https://tools.ietf.org/html/bcp47) |
223
+ | grammars | SpeechGrammarList | null | Grammars understood by the recognition [JSpeech Grammar Format](https://www.w3.org/TR/jsgf/) |
224
+ | timeout | number | 3000 | Time in ms to wait before discarding the recognition |
225
+ | precision | number | 0.4 | Fuse.js score threshold for **phrase** command keys only (lower = stricter). Single-word commands always use exact lookup. |
226
+ | maxAlternatives | number | 1 | Maximum number of recognition alternatives per segment. Setting this to 3–5 lets the engine surface the correct word as a secondary transcript, which is useful for handling homophones (e.g. _vert_ / _verre_ in French). |
227
+ | continuous | boolean | false | Keep the recognition session open after each result. The session accumulates transcript across segments and stops when the button is clicked again or `silenceTimeout` expires. Commands are not evaluated in continuous mode. |
228
+ | silenceTimeout | number | null | When `continuous` is true, automatically stop the session after this many ms of inactivity following the last recognized result. `null` or `0` disables auto-stop (button click required). |
215
229
  | style | object | null | Styles of the root element if className is not specified |
216
230
  | className | string | null | Class of the root element |
217
231
  | ariaLabel | string | 'start recognition' | Accessible label for the default button |
@@ -223,6 +237,7 @@ See [fuze.js scoring theory](https://fusejs.io/concepts/scoring-theory.html) for
223
237
  | onResult | func | null | Handler called when a result is recognized |
224
238
  | onError | func | null | Handler called when an error occurs |
225
239
  | onNoMatch | func | null | Handler called when no result can be recognized |
240
+ | signal | AbortSignal | null | Optional `AbortSignal` propagated to the underlying `start()` call. Aborting it cancels the in-flight start (e.g. while waiting for microphone permission). |
226
241
 
227
242
  ### `useVocal` hook
228
243
 
@@ -237,7 +252,7 @@ const App = () => {
237
252
  const [isListening, setIsListening] = useState(false)
238
253
  const [result, setResult] = useState('')
239
254
 
240
- const [, { start, subscribe }] = useVocal('fr_FR')
255
+ const [, { start, subscribe }] = useVocal('fr-FR')
241
256
 
242
257
  const _onButtonClick = () => {
243
258
  setIsListening(true)
@@ -286,41 +301,60 @@ const App = () => {
286
301
  #### Signature
287
302
 
288
303
  ```
289
- useVocal(lang, grammars)
304
+ useVocal(lang, grammars, maxAlternatives, continuous)
290
305
  ```
291
306
 
292
- | Args | Type | Default | Description |
293
- | -------- | ----------------- | ------- | ----------------------------------------------------------------------------------------------- |
294
- | lang | string | 'en-US' | Language understood by the recognition [BCP 47 language tag](https://tools.ietf.org/html/bcp47) |
295
- | grammars | SpeechGrammarList | null | Grammars understood by the recognition [JSpeech Grammar Format](https://www.w3.org/TR/jsgf/) |
307
+ | Args | Type | Default | Description |
308
+ | --------------- | ----------------- | ------- | ----------------------------------------------------------------------------------------------- |
309
+ | lang | string | 'en-US' | Language understood by the recognition [BCP 47 language tag](https://tools.ietf.org/html/bcp47) |
310
+ | grammars | SpeechGrammarList | null | Grammars understood by the recognition [JSpeech Grammar Format](https://www.w3.org/TR/jsgf/) |
311
+ | maxAlternatives | number | 1 | Maximum number of recognition alternatives per segment |
312
+ | continuous | boolean | false | Keep the recognition session open after each result |
296
313
 
297
314
  ---
298
315
 
299
316
  #### Return value
300
317
 
301
318
  ```
302
- const [ref, { start, stop, abort, subscribe, unsubscribe, clean }]
319
+ const [ref, { start, stop, abort, subscribe, unsubscribe, clean, isRecording }]
303
320
  ```
304
321
 
305
322
  | Args | Type | Description |
306
323
  | ----------- | ---- | ---------------------------------------------------- |
307
- | ref | Ref | React ref to the SpeechRecognitionWrapper instance |
308
- | start | func | Function to start the recognition |
324
+ | ref | Ref | React ref to the underlying `@untemps/vocal` instance |
325
+ | start | func | Function to start the recognition. Accepts an optional `{ signal }` argument — an `AbortSignal` propagated to the underlying `start()` call. Returns the underlying `vocal.start()` promise (resolves once the session starts, rejects on microphone/permission errors). |
309
326
  | stop | func | Function to stop the recognition |
310
327
  | abort | func | Function to abort the recognition |
311
328
  | subscribe | func | Function to subscribe to recognition events |
312
329
  | unsubscribe | func | Function to unsubscribe to recognition events |
313
330
  | clean | func | Function to clean subscription to recognition events |
331
+ | isRecording | bool | Reactive flag mirroring whether a session is active. `true` between `start()` and the next `end`/`error` event. Updated optimistically on `start()` so the UI re-renders at click time. |
332
+
333
+ #### Cancelling a start in flight
334
+
335
+ Both `<Vocal signal={...}>` and `useVocal().start({ signal })` accept an `AbortSignal`. Aborting the controller while the browser is still resolving microphone permission cancels the start cleanly — no `start` event is dispatched.
336
+
337
+ ```javascript
338
+ const controller = new AbortController()
339
+
340
+ // Cancel pending recognition after 2s of waiting for permission
341
+ setTimeout(() => controller.abort(), 2000)
342
+
343
+ const [, { start }] = useVocal('en-US')
344
+ start({ signal: controller.signal })
345
+ ```
314
346
 
315
347
  ### Browser support flag
316
348
 
317
349
  #### Basic usage
318
350
 
351
+ `isSupported` is a function that returns `true` when the browser supports the Web Speech API (along with the Permissions and MediaDevices APIs that `@untemps/vocal` relies on). It is safe to call during server-side rendering — it returns `false` when `window` is undefined.
352
+
319
353
  ```javascript
320
354
  import Vocal, { isSupported } from '@untemps/react-vocal'
321
355
 
322
356
  const App = () => {
323
- return isSupported ? <Vocal /> : <p>Your browser does not support Web Speech API</p>
357
+ return isSupported() ? <Vocal /> : <p>Your browser does not support Web Speech API</p>
324
358
  }
325
359
  ```
326
360
 
package/dist/index.cjs ADDED
@@ -0,0 +1,7 @@
1
+ Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:`Module`}});var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),s=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},c=(n,r,a)=>(a=n==null?{}:e(i(n)),s(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));let l=require(`react`);l=c(l,1);var u=()=>!!navigator.permissions,d=()=>!!navigator.mediaDevices,f=async(e,t,{signal:n}={})=>{if(!u()||!d())throw new DOMException(`Navigator API: permissions or Navigator API: mediaDevices not supported`,`NOT_SUPPORTED_ERR`);if(n?.throwIfAborted(),(await navigator.permissions.query({name:e})).state===`denied`)throw new DOMException(`Permission denied`,`NOT_ALLOWED_ERR`);n?.throwIfAborted();let r=navigator.mediaDevices.getUserMedia(t);if(!n)return r;let i,a=new Promise((e,t)=>{i=()=>t(n.reason),n.addEventListener(`abort`,i,{once:!0})});return Promise.race([r,a]).finally(()=>{n.removeEventListener(`abort`,i)})},p={AUDIO_END:`audioend`,AUDIO_START:`audiostart`,END:`end`,ERROR:`error`,NO_MATCH:`nomatch`,RESULT:`result`,SOUND_END:`soundend`,SOUND_START:`soundstart`,SPEECH_END:`speechend`,SPEECH_START:`speechstart`,START:`start`},m=1e3,h=new Set([`not-allowed`,`service-not-allowed`,`audio-capture`]),g={grammars:null,lang:`en-US`,continuous:!1,interimResults:!1,maxAlternatives:1},_=()=>{if(!(typeof window>`u`))return window.SpeechRecognition??window.webkitSpeechRecognition??window.mozSpeechRecognition??window.msSpeechRecognition},v=()=>window.SpeechGrammarList??window.webkitSpeechGrammarList??window.mozSpeechGrammarList??window.msSpeechGrammarList,y=e=>e.reduce((e,t)=>(t.confidence??0)>(e.confidence??0)?t:e),b=e=>{let t=e.slice();return Object.defineProperty(t,`isFinal`,{value:!0}),Object.defineProperty(t,`item`,{value:e=>t[e]}),t},x=e=>{let t=e.slice();return Object.defineProperty(t,`item`,{value:e=>t[e]}),t},S=e=>Object.values(p).includes(e),C=e=>`Unknown event type "${e}". Valid types are: ${Object.values(p).join(`, `)}.`,w=()=>!!_()&&!!u()&&!!d(),T=e=>{let t=_();if(!t)throw new DOMException(`SpeechRecognition not supported`,`NOT_SUPPORTED_ERR`);let n=new t,r={},i=!1,a=!1,o=0,s=null,c=!1,l=[],u={...g,...e??{}};if(n.lang=u.lang,n.continuous=u.continuous,n.interimResults=u.interimResults,n.maxAlternatives=u.maxAlternatives,u.grammars)n.grammars=u.grammars;else{let e=v();n.grammars=e?new e:null}let d=()=>{s!==null&&(clearTimeout(s),s=null),c=!1},w=()=>!!n&&!a&&n.continuous,T=()=>{s=null;try{n.start(),o=Date.now()}catch{c=!1,i=!1}},E=()=>{let e=l;if(l=[],e.length===0||!r[p.RESULT]?.length)return;let t=e.map(e=>y(Array.from(e)).transcript).join(` `).trim(),n=Object.assign(new Event(p.RESULT),{resultIndex:0,results:x(e)});[...r[p.RESULT]].forEach(({callback:e})=>{e(n,t,[t])})},D=[[p.END,e=>{if(w()){let t=Math.max(0,m-(Date.now()-o));c=!0,s=setTimeout(T,t),e.stopImmediatePropagation();return}E(),i=!1}],[p.START,e=>{c&&(e.stopImmediatePropagation(),queueMicrotask(()=>{c=!1}))}],[p.ERROR,e=>{h.has(e.error)&&(a=!0,d(),i=!1)}],[p.RESULT,e=>{if(!u.continuous)return;let t=e,n=t.results?.[t.resultIndex];n?.isFinal&&l.push(b(Array.from(n)))}]];D.forEach(([e,t])=>n.addEventListener(e,t));let O=async({signal:e}={})=>{if(n)try{let t=await f(`microphone`,{audio:!0},{signal:e});if(e?.aborted||!n)return;if(!t)throw Error(`Unable to retrieve the stream from media device`);a=!1,l=[],n.start(),i=!0,o=Date.now()}catch(e){if(e instanceof Error&&e.name===`AbortError`)return;throw e}},k=()=>{n&&(a=!0,d(),n.stop(),i=!1)},A=()=>{n&&(a=!0,d(),l=[],n.abort(),i=!1)},j=(e,t)=>{if(!S(e))throw Error(C(e));if(!n)return;let i=n=>{if(c&&(e===p.END||e===p.START))return;if(e!==p.RESULT){t(n);return}let r=n;if(!(r.results?.length>0)||r.resultIndex>=r.results.length){t(n);return}let i=r.results[r.resultIndex];if(u.continuous&&i.isFinal)return;let a=Array.from(i);t(n,y(a).transcript,a.map(e=>e.transcript))};n.addEventListener(e,i),r[e]||(r[e]=[]),r[e].push({callback:t,handler:i})},M=(e,t)=>{if(!S(e))throw Error(C(e));if(!(!n||!r[e]))if(t!==void 0){let i=r[e].findIndex(e=>e.callback===t);i!==-1&&(n.removeEventListener(e,r[e][i].handler),r[e].splice(i,1),r[e].length===0&&delete r[e])}else r[e].forEach(({handler:t})=>n.removeEventListener(e,t)),delete r[e]};return{get isRecording(){return i},start:O,stop:k,abort:A,on:j,off:M,cleanup:()=>{k(),Object.keys(r).forEach(e=>M(e)),D.forEach(([e,t])=>n?.removeEventListener(e,t)),n=null}}},E=e=>typeof e==`function`,D=(e=`en-US`,t=null,n=1,r=!1,i=null)=>{let a=(0,l.useRef)(null),[o,s]=(0,l.useState)(!1),c=(0,l.useMemo)(()=>w(),[]);return(0,l.useEffect)(()=>{if(c){let o=i||T({lang:e,grammars:t,maxAlternatives:n,continuous:r});a.current=o;let c=()=>s(!0),l=()=>s(!1);return o.on(`start`,c),o.on(`end`,l),o.on(`error`,l),()=>{o.off(`start`,c),o.off(`end`,l),o.off(`error`,l),o.abort(),o.cleanup(),s(!1)}}},[e,t,n,r,i,c]),[a,{start:(0,l.useCallback)(e=>{if(a.current)return s(!0),a.current.start(e)},[]),stop:(0,l.useCallback)(()=>{a.current&&a.current.stop()},[]),abort:(0,l.useCallback)(()=>{a.current&&a.current.abort()},[]),subscribe:(0,l.useCallback)((e,t)=>{a.current&&a.current.on(e,t)},[]),unsubscribe:(0,l.useCallback)((e,t)=>{a.current&&a.current.off(e,t)},[]),clean:(0,l.useCallback)(()=>{a.current&&a.current.cleanup()},[]),isRecording:o}]},O=(e,t=0)=>{let n=(0,l.useRef)(-1),r=(0,l.useCallback)(()=>{clearTimeout(n.current),n.current=-1},[]),i=(0,l.useCallback)(()=>{r(),n.current=setTimeout(e,t)},[e,t,r]);return(0,l.useEffect)(()=>r,[r]),[i,r]},k=(e,t=.4)=>{let n=(0,l.useMemo)(()=>e?Object.entries(e).reduce((e,[t,n])=>({...e,[t.toLowerCase()]:n}),{}):{},[e]),r=(0,l.useMemo)(()=>Object.keys(n),[n]),i=(0,l.useMemo)(()=>r.some(e=>e.includes(` `)),[r]),a=(0,l.useRef)(null);return(0,l.useEffect)(()=>{if(!i){a.current=null;return}let e=!1;return import(`fuse.js`).then(t=>{e||(a.current=new(t.default??t)(r,{includeScore:!0,ignoreLocation:!0}))}).catch(()=>{e||process.env.NODE_ENV!==`production`&&console.warn(`[react-vocal] fuse.js is not installed. Phrase command keys will fall back to exact matching. Install fuse.js to enable fuzzy matching: npm install fuse.js`)}),()=>{e=!0}},[i,r]),e=>{if(!r.length)return null;if(!i){let t=e.trim().split(/\s+/),r=t.length>1?t:[e.trim()];for(let e of r){let t=e.toLowerCase();if(t in n)return n[t]?.(e,t)}return null}let o=a.current;if(o){let r=o.search(e).filter(e=>e.score<t);if(r?.length){let t=r[0].item.toLowerCase();return n[t]?.(e,t)}}else{let t=e.toLowerCase(),i=r.find(e=>t.includes(e)||e.includes(t));if(i)return n[i]?.(e,i)}return null}},A=o((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.fragment`);function r(e,n,r){var i=null;if(r!==void 0&&(i=``+r),n.key!==void 0&&(i=``+n.key),`key`in n)for(var a in r={},n)a!==`key`&&(r[a]=n[a]);else r=n;return n=r.ref,{$$typeof:t,type:e,key:i,ref:n===void 0?null:n,props:r}}e.Fragment=n,e.jsx=r,e.jsxs=r})),j=o((e=>{process.env.NODE_ENV!==`production`&&(function(){function t(e){if(e==null)return null;if(typeof e==`function`)return e.$$typeof===O?null:e.displayName||e.name||null;if(typeof e==`string`)return e;switch(e){case _:return`Fragment`;case y:return`Profiler`;case v:return`StrictMode`;case C:return`Suspense`;case w:return`SuspenseList`;case D:return`Activity`}if(typeof e==`object`)switch(typeof e.tag==`number`&&console.error(`Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue.`),e.$$typeof){case g:return`Portal`;case x:return e.displayName||`Context`;case b:return(e._context.displayName||`Context`)+`.Consumer`;case S:var n=e.render;return e=e.displayName,e||=(e=n.displayName||n.name||``,e===``?`ForwardRef`:`ForwardRef(`+e+`)`),e;case T:return n=e.displayName||null,n===null?t(e.type)||`Memo`:n;case E:n=e._payload,e=e._init;try{return t(e(n))}catch{}}return null}function n(e){return``+e}function r(e){try{n(e);var t=!1}catch{t=!0}if(t){t=console;var r=t.error,i=typeof Symbol==`function`&&Symbol.toStringTag&&e[Symbol.toStringTag]||e.constructor.name||`Object`;return r.call(t,`The provided key is an unsupported type %s. This value must be coerced to a string before using it here.`,i),n(e)}}function i(e){if(e===_)return`<>`;if(typeof e==`object`&&e&&e.$$typeof===E)return`<...>`;try{var n=t(e);return n?`<`+n+`>`:`<...>`}catch{return`<...>`}}function a(){var e=k.A;return e===null?null:e.getOwner()}function o(){return Error(`react-stack-top-frame`)}function s(e){if(A.call(e,`key`)){var t=Object.getOwnPropertyDescriptor(e,`key`).get;if(t&&t.isReactWarning)return!1}return e.key!==void 0}function c(e,t){function n(){N||(N=!0,console.error("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://react.dev/link/special-props)",t))}n.isReactWarning=!0,Object.defineProperty(e,`key`,{get:n,configurable:!0})}function l(){var e=t(this.type);return P[e]||(P[e]=!0,console.error(`Accessing element.ref was removed in React 19. ref is now a regular prop. It will be removed from the JSX Element type in a future release.`)),e=this.props.ref,e===void 0?null:e}function u(e,t,n,r,i,a){var o=n.ref;return e={$$typeof:h,type:e,key:t,props:n,_owner:r},(o===void 0?null:o)===null?Object.defineProperty(e,`ref`,{enumerable:!1,value:null}):Object.defineProperty(e,`ref`,{enumerable:!1,get:l}),e._store={},Object.defineProperty(e._store,`validated`,{configurable:!1,enumerable:!1,writable:!0,value:0}),Object.defineProperty(e,`_debugInfo`,{configurable:!1,enumerable:!1,writable:!0,value:null}),Object.defineProperty(e,`_debugStack`,{configurable:!1,enumerable:!1,writable:!0,value:i}),Object.defineProperty(e,`_debugTask`,{configurable:!1,enumerable:!1,writable:!0,value:a}),Object.freeze&&(Object.freeze(e.props),Object.freeze(e)),e}function d(e,n,i,o,l,d){var p=n.children;if(p!==void 0)if(o)if(j(p)){for(o=0;o<p.length;o++)f(p[o]);Object.freeze&&Object.freeze(p)}else console.error(`React.jsx: Static children should always be an array. You are likely explicitly calling React.jsxs or React.jsxDEV. Use the Babel transform instead.`);else f(p);if(A.call(n,`key`)){p=t(e);var m=Object.keys(n).filter(function(e){return e!==`key`});o=0<m.length?`{key: someKey, `+m.join(`: ..., `)+`: ...}`:`{key: someKey}`,L[p+o]||(m=0<m.length?`{`+m.join(`: ..., `)+`: ...}`:`{}`,console.error(`A props object containing a "key" prop is being spread into JSX:
2
+ let props = %s;
3
+ <%s {...props} />
4
+ React keys must be passed directly to JSX without using spread:
5
+ let props = %s;
6
+ <%s key={someKey} {...props} />`,o,p,m,p),L[p+o]=!0)}if(p=null,i!==void 0&&(r(i),p=``+i),s(n)&&(r(n.key),p=``+n.key),`key`in n)for(var h in i={},n)h!==`key`&&(i[h]=n[h]);else i=n;return p&&c(i,typeof e==`function`?e.displayName||e.name||`Unknown`:e),u(e,p,i,a(),l,d)}function f(e){p(e)?e._store&&(e._store.validated=1):typeof e==`object`&&e&&e.$$typeof===E&&(e._payload.status===`fulfilled`?p(e._payload.value)&&e._payload.value._store&&(e._payload.value._store.validated=1):e._store&&(e._store.validated=1))}function p(e){return typeof e==`object`&&!!e&&e.$$typeof===h}var m=require(`react`),h=Symbol.for(`react.transitional.element`),g=Symbol.for(`react.portal`),_=Symbol.for(`react.fragment`),v=Symbol.for(`react.strict_mode`),y=Symbol.for(`react.profiler`),b=Symbol.for(`react.consumer`),x=Symbol.for(`react.context`),S=Symbol.for(`react.forward_ref`),C=Symbol.for(`react.suspense`),w=Symbol.for(`react.suspense_list`),T=Symbol.for(`react.memo`),E=Symbol.for(`react.lazy`),D=Symbol.for(`react.activity`),O=Symbol.for(`react.client.reference`),k=m.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,A=Object.prototype.hasOwnProperty,j=Array.isArray,M=console.createTask?console.createTask:function(){return null};m={react_stack_bottom_frame:function(e){return e()}};var N,P={},F=m.react_stack_bottom_frame.bind(m,o)(),I=M(i(o)),L={};e.Fragment=_,e.jsx=function(e,t,n){var r=1e4>k.recentlyCreatedOwnerStacks++;return d(e,t,n,!1,r?Error(`react-stack-top-frame`):F,r?M(i(e)):I)},e.jsxs=function(e,t,n){var r=1e4>k.recentlyCreatedOwnerStacks++;return d(e,t,n,!0,r?Error(`react-stack-top-frame`):F,r?M(i(e)):I)}})()})),M=o(((e,t)=>{process.env.NODE_ENV===`production`?t.exports=A():t.exports=j()}))(),N=({color:e=`black`,activeColor:t=`red`,isActive:n=!1})=>(0,M.jsx)(`svg`,{"data-testid":`__icon-root__`,xmlns:`http://www.w3.org/2000/svg`,width:`100%`,height:`100%`,viewBox:`0 0 24 24`,"aria-hidden":`true`,children:(0,M.jsxs)(`g`,{children:[(0,M.jsx)(`path`,{"data-testid":`__icon-path__`,fill:e,d:`M12 14c1.66 0 2.99-1.34 2.99-3L15 5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5.3-3c0 3-2.54 5.1-5.3 5.1S6.7 14 6.7 11H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c3.28-.48 6-3.3 6-6.72h-1.7z`}),n&&(0,M.jsx)(`circle`,{"data-testid":`__icon-active__`,fill:t,cx:`16`,cy:`4`,r:`4`})]})}),P=(e,t)=>{for(let n of e)for(let e of n)if(t(e.transcript??``)!==null)return},F=({children:e,commands:t=null,lang:n=`en-US`,grammars:r=null,timeout:i=3e3,silenceTimeout:a=null,precision:o=.4,maxAlternatives:s=1,continuous:c=!1,ariaLabel:u=`start recognition`,style:d=null,className:f=null,outlineStyle:p=`2px solid`,onStart:m=null,onEnd:h=null,onSpeechStart:g=null,onSpeechEnd:_=null,onResult:v=null,onError:y=null,onNoMatch:b=null,signal:x=null,__rsInstance:S})=>{let C=(0,l.useRef)(null),T=(0,l.useMemo)(()=>w(),[]),[,{start:A,stop:j,subscribe:F,unsubscribe:I,isRecording:L}]=D(n,r,s,c,S),R=k(t,o),z=(0,l.useRef)({});z.current={onStart:m,onEnd:h,onSpeechStart:g,onSpeechEnd:_,onResult:v,onError:y,onNoMatch:b};let B=(0,l.useRef)(c);B.current=c;let V=(0,l.useRef)(R);V.current=R;let H=(0,l.useRef)(null),U=(0,l.useRef)(null),W=(0,l.useRef)(!1),G=(0,l.useRef)(a);G.current=a;let ee=(0,l.useCallback)(()=>U.current?.(),[]),[K,q]=O(ee,i),[te,J]=O(ee,a??0),Y=(0,l.useCallback)(()=>{try{j()}catch(e){z.current.onError?.(e),H.current?.()}},[j]),ne=(0,l.useCallback)(e=>{K(),z.current.onStart?.(e)},[K]),re=(0,l.useCallback)(e=>{q(),z.current.onSpeechStart?.(e)},[q]),ie=(0,l.useCallback)(e=>{K(),B.current&&G.current>0&&te(),z.current.onSpeechEnd?.(e)},[K,te]),ae=(0,l.useCallback)((e,t)=>{q(),B.current||(P(e?.results??[],V.current),Y()),z.current.onResult?.(t,e)},[q,Y]),X=(0,l.useCallback)(e=>{Y(),z.current.onError?.(e)},[Y]),oe=(0,l.useCallback)(e=>{q(),Y(),z.current.onNoMatch?.(e)},[q,Y]),Z=(0,l.useCallback)(e=>{if(!W.current){W.current=!0,q(),J();try{Y(),H.current?.()}finally{W.current=!1,z.current.onEnd?.(e)}}},[q,J,Y]);U.current=Z;let Q=(0,l.useMemo)(()=>({start:ne,end:Z,speechstart:re,speechend:ie,result:ae,error:X,nomatch:oe}),[ne,Z,re,ie,ae,X,oe]);H.current=()=>Object.entries(Q).forEach(([e,t])=>I?.(e,t));let $=(0,l.useCallback)(()=>{try{J(),Object.entries(Q).forEach(([e,t])=>F(e,t)),A({signal:x})?.catch?.(X)}catch(e){X(e)}},[Q,F,A,J,X,x]),se=()=>{!f&&p&&(C.current.style.outline=p)},ce=()=>{!f&&p&&(C.current.style.outline=`none`)};return T?E(e)?e($,Y,L):(0,l.isValidElement)(e)?(0,l.cloneElement)(e,{...!L&&{onClick:$}}):(0,M.jsx)(`button`,{"data-testid":`__vocal-root__`,ref:C,"aria-label":u,"aria-pressed":L,style:f?null:{width:24,height:24,backgroundColor:`transparent`,border:`none`,padding:0,cursor:!c&&L?`default`:`pointer`,...d},className:f,onFocus:se,onBlur:ce,onClick:L?Y:$,children:(0,M.jsx)(N,{isActive:L,color:`#aaa`})}):null};exports.default=F,exports.isSupported=w,exports.useVocal=D;
7
+ //# sourceMappingURL=index.cjs.map