@voiceinput/react 0.1.0-beta.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/dist/index.cjs ADDED
@@ -0,0 +1,701 @@
1
+ "use client";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ let _voiceinput_core = require("@voiceinput/core");
4
+ let react = require("react");
5
+ let react_jsx_runtime = require("react/jsx-runtime");
6
+ //#region src/coordinator.ts
7
+ var VoiceInputCoordinator = class {
8
+ #active;
9
+ #requested;
10
+ #generation = 0;
11
+ #queue = Promise.resolve();
12
+ activate(session) {
13
+ const generation = ++this.#generation;
14
+ this.#requested = session;
15
+ const activation = this.#queue.then(async () => {
16
+ if (!this.#isCurrentRequest(session, generation)) return false;
17
+ const previous = this.#active;
18
+ if (previous !== void 0 && previous !== session) {
19
+ try {
20
+ await previous.stop("replaced");
21
+ } catch (error) {
22
+ reportUnhandledError(error);
23
+ }
24
+ if (this.#active === previous) this.#active = void 0;
25
+ }
26
+ if (!this.#isCurrentRequest(session, generation)) return false;
27
+ this.#active = session;
28
+ return true;
29
+ });
30
+ this.#queue = activation.then(() => {}, () => {});
31
+ return activation;
32
+ }
33
+ release(session) {
34
+ this.cancel(session);
35
+ if (this.#active === session) this.#active = void 0;
36
+ }
37
+ cancel(session) {
38
+ if (this.#requested === session) {
39
+ this.#requested = void 0;
40
+ this.#generation += 1;
41
+ }
42
+ }
43
+ #isCurrentRequest(session, generation) {
44
+ return this.#requested === session && this.#generation === generation;
45
+ }
46
+ };
47
+ function reportUnhandledError(error) {
48
+ const reportError = globalThis.reportError;
49
+ reportError?.(error);
50
+ }
51
+ //#endregion
52
+ //#region src/context.tsx
53
+ const VoiceInputContext = (0, react.createContext)(null);
54
+ function VoiceInputProvider({ provider, audioSource, children }) {
55
+ const browserAudioSource = (0, react.useMemo)(() => (0, _voiceinput_core.createBrowserAudioSource)(), []);
56
+ const resolvedAudioSource = audioSource ?? browserAudioSource;
57
+ const coordinator = (0, react.useMemo)(() => new VoiceInputCoordinator(), []);
58
+ const value = (0, react.useMemo)(() => ({
59
+ provider,
60
+ audioSource: resolvedAudioSource,
61
+ coordinator
62
+ }), [
63
+ coordinator,
64
+ provider,
65
+ resolvedAudioSource
66
+ ]);
67
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VoiceInputContext.Provider, {
68
+ value,
69
+ children
70
+ });
71
+ }
72
+ //#endregion
73
+ //#region src/use-voice-input.ts
74
+ const ACTIVE_KEYS = /* @__PURE__ */ new Set(["Enter", " "]);
75
+ const useIsomorphicLayoutEffect = typeof window === "undefined" ? react.useEffect : react.useLayoutEffect;
76
+ function useVoiceInput(options = {}) {
77
+ return useVoiceInputInternal(options);
78
+ }
79
+ function useVoiceInputInternal(options = {}, dispatchInput = false) {
80
+ const context = (0, react.useContext)(VoiceInputContext);
81
+ const provider = options.provider ?? context?.provider;
82
+ if (provider === void 0) throw new _voiceinput_core.VoiceInputError({
83
+ code: "invalid-configuration",
84
+ message: "useVoiceInput requires a provider option or a parent VoiceInputProvider."
85
+ });
86
+ const controlled = options.value !== void 0 || options.onValueChange !== void 0;
87
+ if (controlled && (typeof options.value !== "string" || typeof options.onValueChange !== "function")) throw new _voiceinput_core.VoiceInputError({
88
+ code: "invalid-configuration",
89
+ message: "Controlled voice input requires both value and onValueChange options."
90
+ });
91
+ if ((0, react.useRef)(controlled).current !== controlled) throw new _voiceinput_core.VoiceInputError({
92
+ code: "invalid-configuration",
93
+ message: "A voice field cannot switch between controlled and uncontrolled modes. Remount it with a new key."
94
+ });
95
+ const latest = (0, react.useRef)(options);
96
+ latest.current = options;
97
+ const disabled = options.disabled ?? false;
98
+ const disabledRef = (0, react.useRef)(disabled);
99
+ disabledRef.current = disabled;
100
+ const disposedRef = (0, react.useRef)(false);
101
+ const selectionCapturedRef = (0, react.useRef)(false);
102
+ const heldPointerRef = (0, react.useRef)(null);
103
+ const heldKeyRef = (0, react.useRef)(null);
104
+ const targetNodeRef = (0, react.useRef)(null);
105
+ const browserAudioSource = (0, react.useMemo)(() => (0, _voiceinput_core.createBrowserAudioSource)(), []);
106
+ const audioSource = options.audioSource ?? context?.audioSource ?? browserAudioSource;
107
+ const standaloneCoordinator = (0, react.useMemo)(() => new VoiceInputCoordinator(), []);
108
+ const coordinator = context?.coordinator ?? standaloneCoordinator;
109
+ const vocabularyKey = JSON.stringify(options.vocabulary ?? null);
110
+ const vocabulary = (0, react.useMemo)(() => options.vocabulary === void 0 ? void 0 : [...options.vocabulary], [vocabularyKey]);
111
+ const endpointingKey = JSON.stringify(options.endpointing ?? null);
112
+ const endpointing = (0, react.useMemo)(() => typeof options.endpointing === "object" ? { ...options.endpointing } : options.endpointing, [endpointingKey]);
113
+ const hasTransform = options.transformTranscript !== void 0;
114
+ const [textEngine] = (0, react.useState)(() => {
115
+ return (0, _voiceinput_core.createVoiceInputTextEngine)({
116
+ ...controlled ? { controlled: {
117
+ getValue: () => latest.current.value ?? "",
118
+ dispatchInput,
119
+ onValueChange: (value) => latest.current.onValueChange?.(value)
120
+ } } : {},
121
+ ...options.interimBehavior === void 0 ? {} : { interimBehavior: options.interimBehavior },
122
+ ...!hasTransform ? {} : { transformTranscript: (text) => {
123
+ const transform = latest.current.transformTranscript;
124
+ if (transform === void 0) throw new TypeError("transformTranscript is unavailable.");
125
+ return transform(text);
126
+ } },
127
+ ...options.transformTimeoutMs === void 0 ? {} : { transformTimeoutMs: options.transformTimeoutMs }
128
+ });
129
+ });
130
+ const [session] = (0, react.useState)(() => (0, _voiceinput_core.createVoiceInputSession)({
131
+ provider,
132
+ audioSource,
133
+ textEngine
134
+ }));
135
+ useIsomorphicLayoutEffect(() => {
136
+ textEngine.updateOptions({
137
+ ...options.interimBehavior === void 0 ? {} : { interimBehavior: options.interimBehavior },
138
+ ...options.transformTranscript === void 0 ? {} : { transformTranscript: options.transformTranscript },
139
+ ...options.transformTimeoutMs === void 0 ? {} : { transformTimeoutMs: options.transformTimeoutMs }
140
+ });
141
+ session.updateOptions({
142
+ provider,
143
+ audioSource,
144
+ ...options.language === void 0 ? {} : { language: options.language },
145
+ ...vocabulary === void 0 ? {} : { vocabulary },
146
+ ...endpointing === void 0 ? {} : { endpointing },
147
+ ...options.maxDurationMs === void 0 ? {} : { maxDurationMs: options.maxDurationMs },
148
+ ...options.connectionTimeoutMs === void 0 ? {} : { connectionTimeoutMs: options.connectionTimeoutMs }
149
+ });
150
+ }, [
151
+ session,
152
+ provider,
153
+ audioSource,
154
+ textEngine,
155
+ options.language,
156
+ vocabulary,
157
+ endpointing,
158
+ options.maxDurationMs,
159
+ options.connectionTimeoutMs,
160
+ options.interimBehavior,
161
+ options.transformTranscript,
162
+ options.transformTimeoutMs
163
+ ]);
164
+ const currentTextEngineRef = (0, react.useRef)(textEngine);
165
+ currentTextEngineRef.current = textEngine;
166
+ const undo = (0, react.useCallback)(() => textEngine.undo(), [textEngine]);
167
+ const redo = (0, react.useCallback)(() => textEngine.redo(), [textEngine]);
168
+ const getTextSnapshot = (0, react.useCallback)(() => textEngine.getSnapshot(), [textEngine]);
169
+ (0, react.useEffect)(() => () => {
170
+ queueMicrotask(() => {
171
+ if (disposedRef.current || currentTextEngineRef.current !== textEngine) textEngine.destroy();
172
+ });
173
+ }, [textEngine]);
174
+ const coordinatedSession = (0, react.useMemo)(() => ({ stop: (reason) => session.stop(reason) }), [session]);
175
+ const subscribe = (0, react.useCallback)((listener) => session.subscribe(() => listener()), [session]);
176
+ const getSnapshot = (0, react.useCallback)(() => session.getSnapshot(), [session]);
177
+ const snapshot = (0, react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
178
+ const [isSupported, setIsSupported] = (0, react.useState)(false);
179
+ (0, react.useEffect)(() => {
180
+ setIsSupported((0, _voiceinput_core.getBrowserVoiceInputSupport)().isSupported);
181
+ }, []);
182
+ useIsomorphicLayoutEffect(() => {
183
+ if (controlled) textEngine.reconcileControlledValue(latest.current.value ?? "");
184
+ }, [
185
+ controlled,
186
+ options.value,
187
+ textEngine
188
+ ]);
189
+ (0, react.useEffect)(() => {
190
+ return session.subscribe((event) => {
191
+ dispatchCallback(latest.current, event);
192
+ if (event.type === "status-change" && event.status === "error" || event.type === "stop" || event.type === "cancel") coordinator.release(coordinatedSession);
193
+ });
194
+ }, [
195
+ coordinatedSession,
196
+ coordinator,
197
+ session
198
+ ]);
199
+ const captureSelection = (0, react.useCallback)(() => {
200
+ selectionCapturedRef.current = textEngine.captureSelection() !== null;
201
+ }, [textEngine]);
202
+ const startInternal = (0, react.useCallback)(async (capture = true) => {
203
+ if (disabledRef.current || disposedRef.current || targetNodeRef.current !== null && !textEngine.isWritable()) return;
204
+ if (capture && !selectionCapturedRef.current) captureSelection();
205
+ selectionCapturedRef.current = false;
206
+ if (!await coordinator.activate(coordinatedSession)) return;
207
+ if (disabledRef.current || disposedRef.current || targetNodeRef.current !== null && !textEngine.isWritable()) {
208
+ coordinator.release(coordinatedSession);
209
+ return;
210
+ }
211
+ await session.start();
212
+ const status = session.getSnapshot().status;
213
+ if (status === "idle" || status === "error") coordinator.release(coordinatedSession);
214
+ }, [
215
+ captureSelection,
216
+ coordinatedSession,
217
+ coordinator,
218
+ session,
219
+ textEngine
220
+ ]);
221
+ const start = (0, react.useCallback)(() => startInternal(true), [startInternal]);
222
+ const stop = (0, react.useCallback)(async (reason = "user") => {
223
+ await session.stop(reason);
224
+ coordinator.release(coordinatedSession);
225
+ }, [
226
+ coordinatedSession,
227
+ coordinator,
228
+ session
229
+ ]);
230
+ const cancel = (0, react.useCallback)(async () => {
231
+ await session.cancel();
232
+ coordinator.release(coordinatedSession);
233
+ }, [
234
+ coordinatedSession,
235
+ coordinator,
236
+ session
237
+ ]);
238
+ const toggle = (0, react.useCallback)(async () => {
239
+ const status = session.getSnapshot().status;
240
+ if (status === "idle" || status === "error") await startInternal(true);
241
+ else await stop();
242
+ }, [
243
+ session,
244
+ startInternal,
245
+ stop
246
+ ]);
247
+ const targetRef = (0, react.useCallback)((target) => {
248
+ const previousTarget = targetNodeRef.current;
249
+ targetNodeRef.current = target;
250
+ if (target === null && previousTarget !== null) stop("replaced");
251
+ textEngine.setTarget(target);
252
+ }, [stop, textEngine]);
253
+ (0, react.useEffect)(() => {
254
+ disposedRef.current = false;
255
+ return () => {
256
+ disposedRef.current = true;
257
+ coordinator.cancel(coordinatedSession);
258
+ session.stop("replaced").then(() => coordinator.release(coordinatedSession), () => coordinator.release(coordinatedSession));
259
+ };
260
+ }, [
261
+ coordinatedSession,
262
+ coordinator,
263
+ session
264
+ ]);
265
+ const releaseHold = (0, react.useCallback)(() => {
266
+ if (heldPointerRef.current === null && heldKeyRef.current === null) return;
267
+ heldPointerRef.current = null;
268
+ heldKeyRef.current = null;
269
+ stop();
270
+ }, [stop]);
271
+ (0, react.useEffect)(() => {
272
+ if (disabled) releaseHold();
273
+ }, [disabled, releaseHold]);
274
+ (0, react.useEffect)(() => {
275
+ window.addEventListener("blur", releaseHold);
276
+ return () => window.removeEventListener("blur", releaseHold);
277
+ }, [releaseHold]);
278
+ const [targetWritable, setTargetWritable] = (0, react.useState)(true);
279
+ useIsomorphicLayoutEffect(() => {
280
+ const target = targetNodeRef.current;
281
+ const update = () => setTargetWritable(target === null || textEngine.isWritable());
282
+ update();
283
+ if (target === null) return;
284
+ const observer = new MutationObserver(update);
285
+ observer.observe(target, {
286
+ attributes: true,
287
+ attributeFilter: [
288
+ "disabled",
289
+ "readonly",
290
+ "type"
291
+ ]
292
+ });
293
+ for (let ancestor = target.parentElement; ancestor; ancestor = ancestor.parentElement) observer.observe(ancestor, {
294
+ attributes: true,
295
+ attributeFilter: ["disabled"]
296
+ });
297
+ return () => observer.disconnect();
298
+ });
299
+ (0, react.useEffect)(() => {
300
+ if (disabled || !targetWritable) stop("target-unavailable");
301
+ }, [
302
+ disabled,
303
+ targetWritable,
304
+ stop
305
+ ]);
306
+ const resolvedDisabled = disabled || !targetWritable || !isSupported;
307
+ const active = snapshot.status !== "idle" && snapshot.status !== "error";
308
+ const activationMode = options.activationMode ?? "toggle";
309
+ const triggerProps = (0, react.useMemo)(() => ({
310
+ type: "button",
311
+ disabled: resolvedDisabled,
312
+ "aria-pressed": active,
313
+ onPointerDown(event) {
314
+ if (resolvedDisabled || event.button !== 0) return;
315
+ event.preventDefault();
316
+ if (isStartable(session.getSnapshot().status)) captureSelection();
317
+ if (activationMode === "hold") {
318
+ heldPointerRef.current = event.pointerId;
319
+ try {
320
+ event.currentTarget.setPointerCapture?.(event.pointerId);
321
+ } catch {}
322
+ startInternal(false);
323
+ }
324
+ },
325
+ onPointerUp(event) {
326
+ if (activationMode === "hold" && heldPointerRef.current === event.pointerId) {
327
+ heldPointerRef.current = null;
328
+ try {
329
+ event.currentTarget.releasePointerCapture?.(event.pointerId);
330
+ } catch {}
331
+ stop();
332
+ }
333
+ },
334
+ onPointerCancel(event) {
335
+ if (activationMode === "hold" && heldPointerRef.current === event.pointerId) {
336
+ heldPointerRef.current = null;
337
+ stop();
338
+ }
339
+ },
340
+ onLostPointerCapture() {
341
+ releaseHold();
342
+ },
343
+ onBlur() {
344
+ if (heldKeyRef.current !== null) releaseHold();
345
+ },
346
+ onClick(event) {
347
+ if (resolvedDisabled || activationMode === "hold") {
348
+ event.preventDefault();
349
+ return;
350
+ }
351
+ toggle();
352
+ },
353
+ onKeyDown(event) {
354
+ if (resolvedDisabled || !ACTIVE_KEYS.has(event.key) || event.repeat) return;
355
+ if (isStartable(session.getSnapshot().status)) captureSelection();
356
+ if (activationMode === "hold") {
357
+ event.preventDefault();
358
+ heldKeyRef.current = event.key;
359
+ startInternal(false);
360
+ }
361
+ },
362
+ onKeyUp(event) {
363
+ if (activationMode === "hold" && heldKeyRef.current === event.key) {
364
+ event.preventDefault();
365
+ heldKeyRef.current = null;
366
+ stop();
367
+ }
368
+ }
369
+ }), [
370
+ activationMode,
371
+ active,
372
+ captureSelection,
373
+ releaseHold,
374
+ resolvedDisabled,
375
+ session,
376
+ startInternal,
377
+ stop,
378
+ toggle
379
+ ]);
380
+ const getTriggerProps = (0, react.useCallback)((props = {}) => composeTriggerProps(triggerProps, props), [triggerProps]);
381
+ return (0, react.useMemo)(() => ({
382
+ ...snapshot,
383
+ targetRef,
384
+ triggerProps,
385
+ getTriggerProps,
386
+ isSupported,
387
+ getTextSnapshot,
388
+ undo,
389
+ redo,
390
+ start,
391
+ stop,
392
+ cancel,
393
+ toggle
394
+ }), [
395
+ cancel,
396
+ getTriggerProps,
397
+ getTextSnapshot,
398
+ undo,
399
+ redo,
400
+ isSupported,
401
+ snapshot,
402
+ start,
403
+ stop,
404
+ targetRef,
405
+ toggle,
406
+ triggerProps
407
+ ]);
408
+ }
409
+ function composeTriggerProps(trigger, props) {
410
+ return {
411
+ ...props,
412
+ type: props.type ?? trigger.type,
413
+ disabled: props.disabled === true || trigger.disabled,
414
+ "aria-pressed": trigger["aria-pressed"],
415
+ onBlur: composeEventHandlers$1(props.onBlur, trigger.onBlur),
416
+ onClick: composeEventHandlers$1(props.onClick, trigger.onClick),
417
+ onKeyDown: composeEventHandlers$1(props.onKeyDown, trigger.onKeyDown),
418
+ onKeyUp: composeEventHandlers$1(props.onKeyUp, trigger.onKeyUp),
419
+ onLostPointerCapture: composeEventHandlers$1(props.onLostPointerCapture, trigger.onLostPointerCapture),
420
+ onPointerCancel: composeEventHandlers$1(props.onPointerCancel, trigger.onPointerCancel),
421
+ onPointerDown: composeEventHandlers$1(props.onPointerDown, trigger.onPointerDown),
422
+ onPointerUp: composeEventHandlers$1(props.onPointerUp, trigger.onPointerUp)
423
+ };
424
+ }
425
+ function composeEventHandlers$1(consumer, internal) {
426
+ return (event) => {
427
+ consumer?.(event);
428
+ if (!event.defaultPrevented) internal(event);
429
+ };
430
+ }
431
+ function isStartable(status) {
432
+ return status === "idle" || status === "error";
433
+ }
434
+ function dispatchCallback(callbacks, event) {
435
+ callbacks.onEvent?.(event);
436
+ switch (event.type) {
437
+ case "status-change":
438
+ callbacks.onStatusChange?.(event.status, event.previousStatus);
439
+ break;
440
+ case "interim":
441
+ callbacks.onInterimTranscript?.(event.text);
442
+ if (event.transcriptChanged) callbacks.onTranscriptChange?.(event.transcript);
443
+ break;
444
+ case "final":
445
+ callbacks.onFinalTranscriptPart?.(event.text);
446
+ if (event.finalTranscriptChanged) callbacks.onFinalTranscript?.(event.transcript);
447
+ if (event.transcriptChanged) callbacks.onTranscriptChange?.(event.transcript);
448
+ break;
449
+ case "text-limit":
450
+ callbacks.onTextLimit?.(event);
451
+ break;
452
+ case "duration-warning":
453
+ callbacks.onDurationWarning?.(event.remainingMs, event.maxDurationMs);
454
+ break;
455
+ case "stop":
456
+ callbacks.onStop?.(event.reason);
457
+ break;
458
+ case "error": callbacks.onError?.(event.error);
459
+ }
460
+ }
461
+ //#endregion
462
+ //#region src/components.tsx
463
+ const visuallyHiddenStyle = {
464
+ border: 0,
465
+ clip: "rect(0 0 0 0)",
466
+ clipPath: "inset(50%)",
467
+ height: 1,
468
+ margin: -1,
469
+ overflow: "hidden",
470
+ padding: 0,
471
+ position: "absolute",
472
+ whiteSpace: "nowrap",
473
+ width: 1
474
+ };
475
+ const VoiceButton = /* @__PURE__ */ (0, react.forwardRef)(function VoiceButton({ voice: options, ...props }, forwardedRef) {
476
+ const voice = useVoiceInput({
477
+ ...options,
478
+ disabled: props.disabled === true || options?.disabled === true
479
+ });
480
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VoiceButtonElement, {
481
+ ...props,
482
+ ref: forwardedRef,
483
+ voice
484
+ });
485
+ });
486
+ const VoiceInput = /* @__PURE__ */ (0, react.forwardRef)(function VoiceInput({ className, containerClassName, disabled, readOnly, onChange, onValueChange, type = "text", value, voice: options, voiceButtonProps, ...props }, forwardedRef) {
487
+ const voice = useVoiceField({
488
+ disabled: disabled === true || readOnly === true || voiceButtonProps?.disabled === true,
489
+ onValueChange,
490
+ options,
491
+ value
492
+ });
493
+ const targetRef = useComposedTargetRef(voice, forwardedRef);
494
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
495
+ className: joinClassNames("voiceinput-field", containerClassName),
496
+ ...voiceDataAttributes(voice),
497
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
498
+ ...props,
499
+ ref: targetRef,
500
+ className: joinClassNames("voiceinput-field__input", className),
501
+ disabled,
502
+ readOnly,
503
+ onChange: (event) => {
504
+ onValueChange?.(event.currentTarget.value);
505
+ onChange?.(event);
506
+ },
507
+ type,
508
+ value
509
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VoiceButtonElement, {
510
+ ...voiceButtonProps,
511
+ className: joinClassNames("voiceinput-field__button", voiceButtonProps?.className),
512
+ voice
513
+ })]
514
+ });
515
+ });
516
+ const VoiceTextarea = /* @__PURE__ */ (0, react.forwardRef)(function VoiceTextarea({ className, containerClassName, disabled, readOnly, onChange, onValueChange, value, voice: options, voiceButtonProps, ...props }, forwardedRef) {
517
+ const voice = useVoiceField({
518
+ disabled: disabled === true || readOnly === true || voiceButtonProps?.disabled === true,
519
+ onValueChange,
520
+ options,
521
+ value
522
+ });
523
+ const targetRef = useComposedTargetRef(voice, forwardedRef);
524
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
525
+ className: joinClassNames("voiceinput-field voiceinput-field--textarea", containerClassName),
526
+ ...voiceDataAttributes(voice),
527
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
528
+ ...props,
529
+ ref: targetRef,
530
+ className: joinClassNames("voiceinput-field__input", className),
531
+ disabled,
532
+ readOnly,
533
+ onChange: (event) => {
534
+ onValueChange?.(event.currentTarget.value);
535
+ onChange?.(event);
536
+ },
537
+ value
538
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VoiceButtonElement, {
539
+ ...voiceButtonProps,
540
+ className: joinClassNames("voiceinput-field__button", voiceButtonProps?.className),
541
+ voice
542
+ })]
543
+ });
544
+ });
545
+ const VoiceButtonElement = /* @__PURE__ */ (0, react.forwardRef)(function VoiceButtonElement({ announce = true, children, className, disabled, getAnnouncement = defaultAnnouncement, onBlur, onClick, onKeyDown, onKeyUp, onLostPointerCapture, onPointerCancel, onPointerDown, onPointerUp, type, voice, ...props }, forwardedRef) {
546
+ const announcementId = (0, react.useId)();
547
+ const active = isActive(voice);
548
+ const label = defaultButtonLabel(voice);
549
+ const describedBy = [props["aria-describedby"], announce && announcementId].filter(Boolean).join(" ");
550
+ const trigger = voice.triggerProps;
551
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
552
+ ...props,
553
+ ...voiceDataAttributes(voice),
554
+ ref: forwardedRef,
555
+ "aria-describedby": describedBy || void 0,
556
+ "aria-label": props["aria-label"] ?? label,
557
+ "aria-pressed": trigger["aria-pressed"],
558
+ className: joinClassNames("voiceinput-button", className),
559
+ disabled: disabled === true || trigger.disabled,
560
+ type: type ?? trigger.type,
561
+ onBlur: composeEventHandlers(onBlur, trigger.onBlur),
562
+ onClick: composeEventHandlers(onClick, trigger.onClick),
563
+ onKeyDown: composeEventHandlers(onKeyDown, trigger.onKeyDown),
564
+ onKeyUp: composeEventHandlers(onKeyUp, trigger.onKeyUp),
565
+ onLostPointerCapture: composeEventHandlers(onLostPointerCapture, trigger.onLostPointerCapture),
566
+ onPointerCancel: composeEventHandlers(onPointerCancel, trigger.onPointerCancel),
567
+ onPointerDown: composeEventHandlers(onPointerDown, trigger.onPointerDown),
568
+ onPointerUp: composeEventHandlers(onPointerUp, trigger.onPointerUp),
569
+ children: typeof children === "function" ? children(voice) : children ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DefaultButtonContent, {
570
+ active,
571
+ label
572
+ })
573
+ }), announce ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
574
+ id: announcementId,
575
+ "aria-live": voice.error === null ? "polite" : "assertive",
576
+ className: "voiceinput-sr-only",
577
+ role: voice.error === null ? "status" : "alert",
578
+ style: visuallyHiddenStyle,
579
+ children: getAnnouncement(voice)
580
+ }) : null] });
581
+ });
582
+ function DefaultButtonContent({ active, label }) {
583
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
584
+ "aria-hidden": "true",
585
+ className: "voiceinput-button__icon",
586
+ children: active ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StopIcon, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MicrophoneIcon, {})
587
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
588
+ className: "voiceinput-button__label",
589
+ children: label
590
+ })] });
591
+ }
592
+ function MicrophoneIcon() {
593
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
594
+ viewBox: "0 0 24 24",
595
+ width: "18",
596
+ height: "18",
597
+ fill: "none",
598
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
599
+ d: "M12 3a3 3 0 0 0-3 3v6a3 3 0 1 0 6 0V6a3 3 0 0 0-3-3Z",
600
+ fill: "currentColor"
601
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
602
+ d: "M6.5 11.5a5.5 5.5 0 0 0 11 0M12 17v4m-3 0h6",
603
+ stroke: "currentColor",
604
+ strokeWidth: "1.75",
605
+ strokeLinecap: "round"
606
+ })]
607
+ });
608
+ }
609
+ function StopIcon() {
610
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
611
+ viewBox: "0 0 24 24",
612
+ width: "18",
613
+ height: "18",
614
+ fill: "none",
615
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
616
+ x: "6",
617
+ y: "6",
618
+ width: "12",
619
+ height: "12",
620
+ rx: "2",
621
+ fill: "currentColor"
622
+ })
623
+ });
624
+ }
625
+ function useVoiceField(options) {
626
+ const controlled = options.value !== void 0 || options.onValueChange !== void 0;
627
+ return useVoiceInputInternal({
628
+ ...options.options,
629
+ disabled: options.disabled === true || options.options?.disabled === true,
630
+ ...controlled && options.value !== void 0 ? { value: options.value } : {},
631
+ ...controlled && options.onValueChange !== void 0 ? { onValueChange: options.onValueChange } : {}
632
+ }, true);
633
+ }
634
+ function useComposedTargetRef(voice, forwardedRef) {
635
+ const { targetRef } = voice;
636
+ const nodeRef = (0, react.useRef)(null);
637
+ (0, react.useImperativeHandle)(forwardedRef, () => nodeRef.current, []);
638
+ return (0, react.useCallback)((node) => {
639
+ nodeRef.current = node;
640
+ targetRef(node);
641
+ }, [targetRef]);
642
+ }
643
+ function voiceDataAttributes(voice) {
644
+ return {
645
+ "data-voiceinput-active": String(isActive(voice)),
646
+ "data-voiceinput-error": voice.error?.code ?? "",
647
+ "data-voiceinput-status": voice.status,
648
+ "data-voiceinput-supported": String(voice.isSupported)
649
+ };
650
+ }
651
+ function isActive(voice) {
652
+ return voice.status !== "error" && voice.status !== "idle";
653
+ }
654
+ function defaultButtonLabel(voice) {
655
+ if (!voice.isSupported) return "Voice input unavailable";
656
+ switch (voice.status) {
657
+ case "requesting-permission": return "Requesting access";
658
+ case "connecting": return "Connecting";
659
+ case "listening": return "Stop voice input";
660
+ case "stopping": return "Finishing";
661
+ case "processing": return "Processing";
662
+ case "error":
663
+ case "idle": return "Start voice input";
664
+ }
665
+ }
666
+ function defaultAnnouncement(voice) {
667
+ if (!voice.isSupported) return "Voice input is unavailable in this browser.";
668
+ if (voice.error !== null) return `Voice input error: ${voice.error.message}`;
669
+ switch (voice.status) {
670
+ case "idle": return "Voice input ready.";
671
+ case "requesting-permission": return "Requesting microphone permission.";
672
+ case "connecting": return "Connecting voice input.";
673
+ case "listening": return "Voice input is listening.";
674
+ case "stopping": return "Finishing voice input.";
675
+ case "processing": return "Processing the transcript.";
676
+ case "error": return "Voice input stopped with an error.";
677
+ }
678
+ }
679
+ function composeEventHandlers(consumer, internal) {
680
+ return (event) => {
681
+ consumer?.(event);
682
+ if (!event.defaultPrevented) internal(event);
683
+ };
684
+ }
685
+ function joinClassNames(...values) {
686
+ return values.filter(Boolean).join(" ");
687
+ }
688
+ //#endregion
689
+ exports.VoiceButton = VoiceButton;
690
+ exports.VoiceInput = VoiceInput;
691
+ Object.defineProperty(exports, "VoiceInputError", {
692
+ enumerable: true,
693
+ get: function() {
694
+ return _voiceinput_core.VoiceInputError;
695
+ }
696
+ });
697
+ exports.VoiceInputProvider = VoiceInputProvider;
698
+ exports.VoiceTextarea = VoiceTextarea;
699
+ exports.useVoiceInput = useVoiceInput;
700
+
701
+ //# sourceMappingURL=index.cjs.map