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