@zentauri-ui/zentauri-components 2.3.1 → 2.3.2

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 (144) hide show
  1. package/README.md +7 -5
  2. package/cli/props.json +664 -0
  3. package/cli/registry.json +12 -0
  4. package/dist/chunk-2RBBXVFA.js +19 -0
  5. package/dist/{chunk-MY3DQVNF.js.map → chunk-2RBBXVFA.js.map} +1 -1
  6. package/dist/{chunk-VMVG2RVZ.mjs → chunk-425DAZTS.mjs} +4 -4
  7. package/dist/{chunk-VMVG2RVZ.mjs.map → chunk-425DAZTS.mjs.map} +1 -1
  8. package/dist/{chunk-RBNZNWYQ.js → chunk-6GP74P4F.js} +6 -6
  9. package/dist/{chunk-RBNZNWYQ.js.map → chunk-6GP74P4F.js.map} +1 -1
  10. package/dist/chunk-DUKHIN2W.js +44 -0
  11. package/dist/chunk-DUKHIN2W.js.map +1 -0
  12. package/dist/chunk-HJ7EFBED.js +86 -0
  13. package/dist/chunk-HJ7EFBED.js.map +1 -0
  14. package/dist/chunk-K2VCZK4I.mjs +75 -0
  15. package/dist/chunk-K2VCZK4I.mjs.map +1 -0
  16. package/dist/{chunk-UN5RRNPV.js → chunk-KVBFWRPF.js} +12 -12
  17. package/dist/{chunk-UN5RRNPV.js.map → chunk-KVBFWRPF.js.map} +1 -1
  18. package/dist/chunk-N4MLFU2Q.mjs +69 -0
  19. package/dist/chunk-N4MLFU2Q.mjs.map +1 -0
  20. package/dist/{chunk-K7UU3K54.js → chunk-NZQA4M35.js} +24 -4
  21. package/dist/chunk-NZQA4M35.js.map +1 -0
  22. package/dist/chunk-OME7XOPN.js +78 -0
  23. package/dist/chunk-OME7XOPN.js.map +1 -0
  24. package/dist/chunk-PVDWJUMF.mjs +34 -0
  25. package/dist/chunk-PVDWJUMF.mjs.map +1 -0
  26. package/dist/{chunk-TTKTPERV.mjs → chunk-R2JXARKB.mjs} +3 -3
  27. package/dist/{chunk-TTKTPERV.mjs.map → chunk-R2JXARKB.mjs.map} +1 -1
  28. package/dist/chunk-SD7YXMNV.js +40 -0
  29. package/dist/chunk-SD7YXMNV.js.map +1 -0
  30. package/dist/{chunk-PJATBFEK.mjs → chunk-TZTUL6C4.mjs} +24 -4
  31. package/dist/chunk-TZTUL6C4.mjs.map +1 -0
  32. package/dist/chunk-V5EE5ATH.mjs +36 -0
  33. package/dist/chunk-V5EE5ATH.mjs.map +1 -0
  34. package/dist/{chunk-STWXN5EM.mjs → chunk-YDD5HQGX.mjs} +3 -3
  35. package/dist/{chunk-STWXN5EM.mjs.map → chunk-YDD5HQGX.mjs.map} +1 -1
  36. package/dist/design-system/facade.js +6 -4
  37. package/dist/design-system/facade.js.map +1 -1
  38. package/dist/design-system/facade.mjs +5 -3
  39. package/dist/design-system/facade.mjs.map +1 -1
  40. package/dist/design-system/index.d.ts +2 -0
  41. package/dist/design-system/index.d.ts.map +1 -1
  42. package/dist/design-system/speech-recognition.d.ts +39 -0
  43. package/dist/design-system/speech-recognition.d.ts.map +1 -0
  44. package/dist/design-system/speech-synthesizer.d.ts +41 -0
  45. package/dist/design-system/speech-synthesizer.d.ts.map +1 -0
  46. package/dist/ui/buttons/animated.js +8 -6
  47. package/dist/ui/buttons/animated.js.map +1 -1
  48. package/dist/ui/buttons/animated.mjs +6 -4
  49. package/dist/ui/buttons/animated.mjs.map +1 -1
  50. package/dist/ui/buttons.js +9 -7
  51. package/dist/ui/buttons.mjs +7 -5
  52. package/dist/ui/data-table.js +19 -17
  53. package/dist/ui/data-table.js.map +1 -1
  54. package/dist/ui/data-table.mjs +9 -7
  55. package/dist/ui/data-table.mjs.map +1 -1
  56. package/dist/ui/dynamic-stepper.js +18 -16
  57. package/dist/ui/dynamic-stepper.js.map +1 -1
  58. package/dist/ui/dynamic-stepper.mjs +7 -5
  59. package/dist/ui/dynamic-stepper.mjs.map +1 -1
  60. package/dist/ui/pagination.js +10 -8
  61. package/dist/ui/pagination.mjs +7 -5
  62. package/dist/ui/speech-recognition/animated/animations.d.ts +8 -0
  63. package/dist/ui/speech-recognition/animated/animations.d.ts.map +1 -0
  64. package/dist/ui/speech-recognition/animated/index.d.ts +4 -0
  65. package/dist/ui/speech-recognition/animated/index.d.ts.map +1 -0
  66. package/dist/ui/speech-recognition/animated/speech-recognition-animated.d.ts +6 -0
  67. package/dist/ui/speech-recognition/animated/speech-recognition-animated.d.ts.map +1 -0
  68. package/dist/ui/speech-recognition/animated/types.d.ts +9 -0
  69. package/dist/ui/speech-recognition/animated/types.d.ts.map +1 -0
  70. package/dist/ui/speech-recognition/animated.js +288 -0
  71. package/dist/ui/speech-recognition/animated.js.map +1 -0
  72. package/dist/ui/speech-recognition/animated.mjs +285 -0
  73. package/dist/ui/speech-recognition/animated.mjs.map +1 -0
  74. package/dist/ui/speech-recognition/index.d.ts +4 -0
  75. package/dist/ui/speech-recognition/index.d.ts.map +1 -0
  76. package/dist/ui/speech-recognition/speech-recognition-base.d.ts +6 -0
  77. package/dist/ui/speech-recognition/speech-recognition-base.d.ts.map +1 -0
  78. package/dist/ui/speech-recognition/speech-recognition.d.ts +2 -0
  79. package/dist/ui/speech-recognition/speech-recognition.d.ts.map +1 -0
  80. package/dist/ui/speech-recognition/types.d.ts +31 -0
  81. package/dist/ui/speech-recognition/types.d.ts.map +1 -0
  82. package/dist/ui/speech-recognition/variants.d.ts +11 -0
  83. package/dist/ui/speech-recognition/variants.d.ts.map +1 -0
  84. package/dist/ui/speech-recognition.js +242 -0
  85. package/dist/ui/speech-recognition.js.map +1 -0
  86. package/dist/ui/speech-recognition.mjs +233 -0
  87. package/dist/ui/speech-recognition.mjs.map +1 -0
  88. package/dist/ui/speech-synthesizer/animated/animations.d.ts +8 -0
  89. package/dist/ui/speech-synthesizer/animated/animations.d.ts.map +1 -0
  90. package/dist/ui/speech-synthesizer/animated/index.d.ts +4 -0
  91. package/dist/ui/speech-synthesizer/animated/index.d.ts.map +1 -0
  92. package/dist/ui/speech-synthesizer/animated/speech-synthesizer-animated.d.ts +6 -0
  93. package/dist/ui/speech-synthesizer/animated/speech-synthesizer-animated.d.ts.map +1 -0
  94. package/dist/ui/speech-synthesizer/animated/types.d.ts +9 -0
  95. package/dist/ui/speech-synthesizer/animated/types.d.ts.map +1 -0
  96. package/dist/ui/speech-synthesizer/animated.js +269 -0
  97. package/dist/ui/speech-synthesizer/animated.js.map +1 -0
  98. package/dist/ui/speech-synthesizer/animated.mjs +266 -0
  99. package/dist/ui/speech-synthesizer/animated.mjs.map +1 -0
  100. package/dist/ui/speech-synthesizer/index.d.ts +4 -0
  101. package/dist/ui/speech-synthesizer/index.d.ts.map +1 -0
  102. package/dist/ui/speech-synthesizer/speech-synthesizer-base.d.ts +6 -0
  103. package/dist/ui/speech-synthesizer/speech-synthesizer-base.d.ts.map +1 -0
  104. package/dist/ui/speech-synthesizer/speech-synthesizer.d.ts +2 -0
  105. package/dist/ui/speech-synthesizer/speech-synthesizer.d.ts.map +1 -0
  106. package/dist/ui/speech-synthesizer/types.d.ts +43 -0
  107. package/dist/ui/speech-synthesizer/types.d.ts.map +1 -0
  108. package/dist/ui/speech-synthesizer/variants.d.ts +13 -0
  109. package/dist/ui/speech-synthesizer/variants.d.ts.map +1 -0
  110. package/dist/ui/speech-synthesizer.js +220 -0
  111. package/dist/ui/speech-synthesizer.js.map +1 -0
  112. package/dist/ui/speech-synthesizer.mjs +211 -0
  113. package/dist/ui/speech-synthesizer.mjs.map +1 -0
  114. package/dist/ui/split-button.js +20 -18
  115. package/dist/ui/split-button.js.map +1 -1
  116. package/dist/ui/split-button.mjs +7 -5
  117. package/dist/ui/split-button.mjs.map +1 -1
  118. package/package.json +1 -1
  119. package/src/design-system/index.ts +2 -0
  120. package/src/design-system/speech-recognition.ts +82 -0
  121. package/src/design-system/speech-synthesizer.ts +90 -0
  122. package/src/ui/speech-recognition/animated/animations.ts +62 -0
  123. package/src/ui/speech-recognition/animated/index.ts +8 -0
  124. package/src/ui/speech-recognition/animated/speech-recognition-animated.tsx +276 -0
  125. package/src/ui/speech-recognition/animated/types.ts +11 -0
  126. package/src/ui/speech-recognition/index.ts +15 -0
  127. package/src/ui/speech-recognition/speech-recognition-base.tsx +276 -0
  128. package/src/ui/speech-recognition/speech-recognition.test.tsx +74 -0
  129. package/src/ui/speech-recognition/speech-recognition.tsx +1 -0
  130. package/src/ui/speech-recognition/types.ts +50 -0
  131. package/src/ui/speech-recognition/variants.ts +47 -0
  132. package/src/ui/speech-synthesizer/animated/animations.ts +62 -0
  133. package/src/ui/speech-synthesizer/animated/index.ts +8 -0
  134. package/src/ui/speech-synthesizer/animated/speech-synthesizer-animated.tsx +260 -0
  135. package/src/ui/speech-synthesizer/animated/types.ts +11 -0
  136. package/src/ui/speech-synthesizer/index.ts +14 -0
  137. package/src/ui/speech-synthesizer/speech-synthesizer-base.tsx +255 -0
  138. package/src/ui/speech-synthesizer/speech-synthesizer.test.tsx +87 -0
  139. package/src/ui/speech-synthesizer/speech-synthesizer.tsx +1 -0
  140. package/src/ui/speech-synthesizer/types.ts +57 -0
  141. package/src/ui/speech-synthesizer/variants.ts +55 -0
  142. package/dist/chunk-K7UU3K54.js.map +0 -1
  143. package/dist/chunk-MY3DQVNF.js +0 -19
  144. package/dist/chunk-PJATBFEK.mjs.map +0 -1
@@ -0,0 +1,276 @@
1
+ "use client";
2
+
3
+ import { motion } from "framer-motion";
4
+ import {
5
+ useCallback,
6
+ useEffect,
7
+ useImperativeHandle,
8
+ useRef,
9
+ useState,
10
+ } from "react";
11
+
12
+ import { cn } from "../../../lib/utils";
13
+
14
+ import type { SpeechRecognitionRef, SpeechRecognitionState } from "../types";
15
+ import {
16
+ speechRecognitionBtnActiveClasses,
17
+ speechRecognitionBtnVariants,
18
+ speechRecognitionStatusClasses,
19
+ speechRecognitionTranscriptClasses,
20
+ speechRecognitionVariants,
21
+ } from "../variants";
22
+
23
+ import { speechRecognitionAnimationPresets } from "./animations";
24
+ import type { SpeechRecognitionAnimatedProps } from "./types";
25
+
26
+ function getSpeechRecognition(): any {
27
+ if (typeof window === "undefined") return undefined;
28
+ return (
29
+ (window as any).SpeechRecognition ?? (window as any).webkitSpeechRecognition
30
+ );
31
+ }
32
+
33
+ function DefaultMic({
34
+ isListening,
35
+ state,
36
+ }: {
37
+ isListening: boolean;
38
+ state: SpeechRecognitionState;
39
+ }) {
40
+ return (
41
+ <svg
42
+ viewBox="0 0 24 24"
43
+ fill="none"
44
+ stroke="currentColor"
45
+ strokeWidth="2"
46
+ strokeLinecap="round"
47
+ strokeLinejoin="round"
48
+ className={`size-5 ${state === "error" ? "text-red-400" : ""}`}
49
+ >
50
+ <path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" />
51
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2" />
52
+ <line x1="12" y1="19" x2="12" y2="22" />
53
+ {isListening && (
54
+ <>
55
+ <line x1="9" y1="20" x2="7" y2="22" />
56
+ <line x1="15" y1="20" x2="17" y2="22" />
57
+ </>
58
+ )}
59
+ </svg>
60
+ );
61
+ }
62
+
63
+ export function SpeechRecognitionAnimated({
64
+ appearance,
65
+ size,
66
+ lang,
67
+ continuous = false,
68
+ interimResults = true,
69
+ autoStart = false,
70
+ onResult,
71
+ onError,
72
+ onStateChange,
73
+ renderMic,
74
+ animation = "pulse",
75
+ className,
76
+ children,
77
+ ...rest
78
+ }: SpeechRecognitionAnimatedProps) {
79
+ const [state, setState] = useState<SpeechRecognitionState>("idle");
80
+ const [transcript, setTranscript] = useState("");
81
+ const [interimText, setInterimText] = useState("");
82
+ const recognitionRef = useRef<any>(null);
83
+ const stateRef = useRef<SpeechRecognitionState>("idle");
84
+ const preset = speechRecognitionAnimationPresets[animation];
85
+
86
+ const setStateSafe = useCallback(
87
+ (newState: SpeechRecognitionState) => {
88
+ stateRef.current = newState;
89
+ setState(newState);
90
+ onStateChange?.(newState);
91
+ },
92
+ [onStateChange],
93
+ );
94
+
95
+ const start = useCallback(() => {
96
+ const SR = getSpeechRecognition();
97
+ if (!SR) {
98
+ setStateSafe("error");
99
+ onError?.("SpeechRecognition is not supported in this browser");
100
+ return;
101
+ }
102
+
103
+ if (recognitionRef.current) {
104
+ try {
105
+ recognitionRef.current.abort();
106
+ } catch {}
107
+ }
108
+
109
+ const recognition = new SR();
110
+ recognition.continuous = continuous;
111
+ recognition.interimResults = interimResults;
112
+ if (lang) recognition.lang = lang;
113
+
114
+ recognition.onstart = () => setStateSafe("listening");
115
+ recognition.onresult = (event: any) => {
116
+ let finalText = "";
117
+ let interimTextBuilder = "";
118
+
119
+ for (let i = event.resultIndex; i < event.results.length; i++) {
120
+ const result = event.results[i];
121
+ if (result.isFinal) {
122
+ finalText += result[0].transcript;
123
+ } else {
124
+ interimTextBuilder += result[0].transcript;
125
+ }
126
+ }
127
+
128
+ if (finalText) {
129
+ setTranscript((prev) => prev + finalText);
130
+ onResult?.({ transcript: finalText, isFinal: true });
131
+ setStateSafe("processing");
132
+ }
133
+ setInterimText(interimTextBuilder);
134
+ if (interimTextBuilder) {
135
+ onResult?.({ transcript: interimTextBuilder, isFinal: false });
136
+ }
137
+ };
138
+ recognition.onerror = (event: { error: string }) => {
139
+ setStateSafe("error");
140
+ onError?.(event.error);
141
+ };
142
+ recognition.onend = () => {
143
+ if (stateRef.current === "listening") {
144
+ setStateSafe("idle");
145
+ }
146
+ };
147
+
148
+ try {
149
+ recognition.start();
150
+ } catch (err) {
151
+ setStateSafe("error");
152
+ onError?.(String(err));
153
+ }
154
+ recognitionRef.current = recognition;
155
+ }, [continuous, interimResults, lang, onError, onResult, setStateSafe]);
156
+
157
+ const stop = useCallback(() => {
158
+ if (recognitionRef.current) {
159
+ try {
160
+ recognitionRef.current.stop();
161
+ } catch {}
162
+ }
163
+ setStateSafe("idle");
164
+ }, [setStateSafe]);
165
+
166
+ const abort = useCallback(() => {
167
+ if (recognitionRef.current) {
168
+ try {
169
+ recognitionRef.current.abort();
170
+ } catch {}
171
+ }
172
+ setTranscript("");
173
+ setInterimText("");
174
+ setStateSafe("idle");
175
+ }, [setStateSafe]);
176
+
177
+ const imperativeHandle: SpeechRecognitionRef = {
178
+ start,
179
+ stop,
180
+ abort,
181
+ get state() {
182
+ return stateRef.current;
183
+ },
184
+ };
185
+
186
+ useImperativeHandle((rest as any).ref, () => imperativeHandle, [
187
+ start,
188
+ stop,
189
+ abort,
190
+ ]);
191
+
192
+ useEffect(() => {
193
+ if (autoStart) start();
194
+ return () => {
195
+ if (recognitionRef.current) {
196
+ try {
197
+ recognitionRef.current.abort();
198
+ } catch {}
199
+ }
200
+ };
201
+ }, []);
202
+
203
+ const isListening = state === "listening";
204
+ const toggleListening = useCallback(() => {
205
+ isListening ? stop() : start();
206
+ }, [isListening, start, stop]);
207
+
208
+ const displayText = transcript + (interimText ? ` ${interimText}` : "");
209
+ const statusText =
210
+ state === "idle"
211
+ ? "Click to start listening"
212
+ : state === "listening"
213
+ ? "Listening..."
214
+ : state === "processing"
215
+ ? "Processing..."
216
+ : "Error occurred";
217
+
218
+ return (
219
+ <motion.div
220
+ data-slot="speech-recognition"
221
+ className={cn(speechRecognitionVariants({ size }), className)}
222
+ {...(rest as any)}
223
+ >
224
+ <div className="flex items-center gap-3">
225
+ <motion.button
226
+ type="button"
227
+ data-slot="speech-recognition-btn"
228
+ className={cn(
229
+ speechRecognitionBtnVariants({ appearance, size }),
230
+ isListening && speechRecognitionBtnActiveClasses,
231
+ )}
232
+ onClick={toggleListening}
233
+ aria-label={isListening ? "Stop listening" : "Start listening"}
234
+ aria-pressed={isListening}
235
+ animate={
236
+ isListening
237
+ ? {
238
+ scale: [1, 1.1, 1],
239
+ transition: { duration: 1, repeat: Infinity },
240
+ }
241
+ : { scale: 1 }
242
+ }
243
+ >
244
+ {renderMic ? (
245
+ renderMic({ isListening, state })
246
+ ) : (
247
+ <DefaultMic isListening={isListening} state={state} />
248
+ )}
249
+ </motion.button>
250
+ <motion.span
251
+ data-slot="speech-recognition-status"
252
+ className={speechRecognitionStatusClasses}
253
+ initial="initial"
254
+ animate={isListening ? "animate" : "initial"}
255
+ variants={preset.variants}
256
+ transition={preset.transition}
257
+ >
258
+ {statusText}
259
+ </motion.span>
260
+ </div>
261
+ {displayText && (
262
+ <motion.p
263
+ data-slot="speech-recognition-transcript"
264
+ className={speechRecognitionTranscriptClasses}
265
+ initial={{ opacity: 0, y: 5 }}
266
+ animate={{ opacity: 1, y: 0 }}
267
+ >
268
+ {displayText}
269
+ </motion.p>
270
+ )}
271
+ {children}
272
+ </motion.div>
273
+ );
274
+ }
275
+
276
+ SpeechRecognitionAnimated.displayName = "SpeechRecognitionAnimated";
@@ -0,0 +1,11 @@
1
+ import type { Ref } from "react";
2
+
3
+ import type { SpeechRecognitionBaseProps } from "../types";
4
+ import type { SpeechRecognitionAnimation } from "./animations";
5
+
6
+ export type { SpeechRecognitionAnimation };
7
+
8
+ export type SpeechRecognitionAnimatedProps = SpeechRecognitionBaseProps & {
9
+ animation?: SpeechRecognitionAnimation;
10
+ ref?: Ref<HTMLDivElement>;
11
+ };
@@ -0,0 +1,15 @@
1
+ "use client";
2
+
3
+ export { SpeechRecognition } from "./speech-recognition";
4
+ export type {
5
+ SpeechRecognitionBaseProps,
6
+ SpeechRecognitionProps,
7
+ SpeechRecognitionRef,
8
+ SpeechRecognitionResult,
9
+ SpeechRecognitionState,
10
+ SpeechRecognitionVariantProps,
11
+ } from "./types";
12
+ export {
13
+ speechRecognitionBtnVariants,
14
+ speechRecognitionVariants,
15
+ } from "./variants";
@@ -0,0 +1,276 @@
1
+ "use client";
2
+
3
+ import {
4
+ useCallback,
5
+ useEffect,
6
+ useImperativeHandle,
7
+ useRef,
8
+ useState,
9
+ } from "react";
10
+
11
+ import { cn } from "../../lib/utils";
12
+
13
+ import type {
14
+ SpeechRecognitionBaseProps,
15
+ SpeechRecognitionRef,
16
+ SpeechRecognitionState,
17
+ } from "./types";
18
+ import {
19
+ speechRecognitionBtnActiveClasses,
20
+ speechRecognitionBtnVariants,
21
+ speechRecognitionStatusClasses,
22
+ speechRecognitionTranscriptClasses,
23
+ speechRecognitionVariants,
24
+ } from "./variants";
25
+
26
+ function getSpeechRecognition(): any {
27
+ if (typeof window === "undefined") return undefined;
28
+ return (
29
+ (window as any).SpeechRecognition ?? (window as any).webkitSpeechRecognition
30
+ );
31
+ }
32
+
33
+ function DefaultMic({
34
+ isListening,
35
+ state,
36
+ }: {
37
+ isListening: boolean;
38
+ state: SpeechRecognitionState;
39
+ }) {
40
+ return (
41
+ <svg
42
+ viewBox="0 0 24 24"
43
+ fill="none"
44
+ stroke="currentColor"
45
+ strokeWidth="2"
46
+ strokeLinecap="round"
47
+ strokeLinejoin="round"
48
+ className={`size-5 ${state === "error" ? "text-red-400" : ""}`}
49
+ >
50
+ <path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" />
51
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2" />
52
+ <line x1="12" y1="19" x2="12" y2="22" />
53
+ {isListening && (
54
+ <>
55
+ <line x1="9" y1="20" x2="7" y2="22" />
56
+ <line x1="15" y1="20" x2="17" y2="22" />
57
+ </>
58
+ )}
59
+ </svg>
60
+ );
61
+ }
62
+
63
+ export function SpeechRecognitionBase({
64
+ appearance,
65
+ size,
66
+ lang,
67
+ continuous = false,
68
+ interimResults = true,
69
+ autoStart = false,
70
+ onResult,
71
+ onError,
72
+ onStateChange,
73
+ renderMic,
74
+ className,
75
+ children,
76
+ ...rest
77
+ }: SpeechRecognitionBaseProps) {
78
+ const [state, setState] = useState<SpeechRecognitionState>("idle");
79
+ const [transcript, setTranscript] = useState("");
80
+ const [interimText, setInterimText] = useState("");
81
+ const recognitionRef = useRef<any>(null);
82
+ const stateRef = useRef<SpeechRecognitionState>("idle");
83
+ const localRef = useRef<HTMLDivElement>(null);
84
+
85
+ const setStateSafe = useCallback(
86
+ (newState: SpeechRecognitionState) => {
87
+ stateRef.current = newState;
88
+ setState(newState);
89
+ onStateChange?.(newState);
90
+ },
91
+ [onStateChange],
92
+ );
93
+
94
+ const start = useCallback(() => {
95
+ const SR = getSpeechRecognition();
96
+ if (!SR) {
97
+ setStateSafe("error");
98
+ onError?.("SpeechRecognition is not supported in this browser");
99
+ return;
100
+ }
101
+
102
+ if (recognitionRef.current) {
103
+ try {
104
+ recognitionRef.current.abort();
105
+ } catch {}
106
+ }
107
+
108
+ const recognition = new SR();
109
+ recognition.continuous = continuous;
110
+ recognition.interimResults = interimResults;
111
+ if (lang) recognition.lang = lang;
112
+
113
+ recognition.onstart = () => {
114
+ setStateSafe("listening");
115
+ };
116
+
117
+ recognition.onresult = (event: any) => {
118
+ let finalText = "";
119
+ let interimTextBuilder = "";
120
+
121
+ for (let i = event.resultIndex; i < event.results.length; i++) {
122
+ const result = event.results[i];
123
+ if (result.isFinal) {
124
+ finalText += result[0].transcript;
125
+ } else {
126
+ interimTextBuilder += result[0].transcript;
127
+ }
128
+ }
129
+
130
+ if (finalText) {
131
+ setTranscript((prev) => prev + finalText);
132
+ onResult?.({ transcript: finalText, isFinal: true });
133
+ setStateSafe("processing");
134
+ }
135
+ setInterimText(interimTextBuilder);
136
+ if (interimTextBuilder) {
137
+ onResult?.({ transcript: interimTextBuilder, isFinal: false });
138
+ }
139
+ };
140
+
141
+ recognition.onerror = (event: { error: string }) => {
142
+ setStateSafe("error");
143
+ onError?.(event.error);
144
+ };
145
+
146
+ recognition.onend = () => {
147
+ if (stateRef.current === "listening") {
148
+ setStateSafe("idle");
149
+ }
150
+ };
151
+
152
+ try {
153
+ recognition.start();
154
+ } catch (err) {
155
+ setStateSafe("error");
156
+ onError?.(String(err));
157
+ }
158
+
159
+ recognitionRef.current = recognition;
160
+ }, [continuous, interimResults, lang, onError, onResult, setStateSafe]);
161
+
162
+ const stop = useCallback(() => {
163
+ if (recognitionRef.current) {
164
+ try {
165
+ recognitionRef.current.stop();
166
+ } catch {}
167
+ }
168
+ setStateSafe("idle");
169
+ }, [setStateSafe]);
170
+
171
+ const abort = useCallback(() => {
172
+ if (recognitionRef.current) {
173
+ try {
174
+ recognitionRef.current.abort();
175
+ } catch {}
176
+ }
177
+ setTranscript("");
178
+ setInterimText("");
179
+ setStateSafe("idle");
180
+ }, [setStateSafe]);
181
+
182
+ const imperativeHandle: SpeechRecognitionRef = {
183
+ start,
184
+ stop,
185
+ abort,
186
+ get state() {
187
+ return stateRef.current;
188
+ },
189
+ };
190
+
191
+ useImperativeHandle((rest as any).ref, () => imperativeHandle, [
192
+ start,
193
+ stop,
194
+ abort,
195
+ ]);
196
+
197
+ useEffect(() => {
198
+ if (autoStart) {
199
+ start();
200
+ }
201
+ return () => {
202
+ if (recognitionRef.current) {
203
+ try {
204
+ recognitionRef.current.abort();
205
+ } catch {}
206
+ }
207
+ };
208
+ }, []);
209
+
210
+ const isListening = state === "listening";
211
+
212
+ const toggleListening = useCallback(() => {
213
+ if (isListening) {
214
+ stop();
215
+ } else {
216
+ start();
217
+ }
218
+ }, [isListening, start, stop]);
219
+
220
+ const displayText = transcript + (interimText ? ` ${interimText}` : "");
221
+
222
+ const statusText =
223
+ state === "idle"
224
+ ? "Click to start listening"
225
+ : state === "listening"
226
+ ? "Listening..."
227
+ : state === "processing"
228
+ ? "Processing..."
229
+ : "Error occurred";
230
+
231
+ return (
232
+ <div
233
+ ref={localRef}
234
+ data-slot="speech-recognition"
235
+ className={cn(speechRecognitionVariants({ size }), className)}
236
+ {...rest}
237
+ >
238
+ <div className="flex items-center gap-3">
239
+ <button
240
+ type="button"
241
+ data-slot="speech-recognition-btn"
242
+ className={cn(
243
+ speechRecognitionBtnVariants({ appearance, size }),
244
+ isListening && speechRecognitionBtnActiveClasses,
245
+ )}
246
+ onClick={toggleListening}
247
+ aria-label={isListening ? "Stop listening" : "Start listening"}
248
+ aria-pressed={isListening}
249
+ >
250
+ {renderMic ? (
251
+ renderMic({ isListening, state })
252
+ ) : (
253
+ <DefaultMic isListening={isListening} state={state} />
254
+ )}
255
+ </button>
256
+ <span
257
+ data-slot="speech-recognition-status"
258
+ className={speechRecognitionStatusClasses}
259
+ >
260
+ {statusText}
261
+ </span>
262
+ </div>
263
+ {displayText && (
264
+ <p
265
+ data-slot="speech-recognition-transcript"
266
+ className={speechRecognitionTranscriptClasses}
267
+ >
268
+ {displayText}
269
+ </p>
270
+ )}
271
+ {children}
272
+ </div>
273
+ );
274
+ }
275
+
276
+ SpeechRecognitionBase.displayName = "SpeechRecognition";
@@ -0,0 +1,74 @@
1
+ import { createRef } from "react";
2
+ import { render } from "@testing-library/react";
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ import { SpeechRecognition } from "./speech-recognition";
6
+
7
+ describe("SpeechRecognition", () => {
8
+ it("should set displayName", () => {
9
+ expect(SpeechRecognition.displayName).toBe("SpeechRecognition");
10
+ });
11
+
12
+ it("should stamp data-slot on the root container", () => {
13
+ const { container } = render(<SpeechRecognition />);
14
+ const root = container.querySelector('[data-slot="speech-recognition"]');
15
+ expect(root).toBeTruthy();
16
+ });
17
+
18
+ it("should render a mic button", () => {
19
+ const { container } = render(<SpeechRecognition />);
20
+ const btn = container.querySelector('[data-slot="speech-recognition-btn"]');
21
+ expect(btn).toBeTruthy();
22
+ });
23
+
24
+ it("should render a status label", () => {
25
+ const { container } = render(<SpeechRecognition />);
26
+ const status = container.querySelector(
27
+ '[data-slot="speech-recognition-status"]',
28
+ );
29
+ expect(status).toBeTruthy();
30
+ expect(status?.textContent).toContain("start listening");
31
+ });
32
+
33
+ it("should render children when provided", () => {
34
+ const { container } = render(
35
+ <SpeechRecognition>
36
+ <div data-testid="child" />
37
+ </SpeechRecognition>,
38
+ );
39
+ expect(container.querySelector('[data-testid="child"]')).toBeTruthy();
40
+ });
41
+
42
+ it("should expose an imperative handle with start, stop, abort methods", () => {
43
+ const ref = createRef<{
44
+ start: () => void;
45
+ stop: () => void;
46
+ abort: () => void;
47
+ state: string;
48
+ }>();
49
+ render(<SpeechRecognition ref={ref as any} />);
50
+ expect(ref.current).toBeTruthy();
51
+ expect(typeof ref.current?.start).toBe("function");
52
+ expect(typeof ref.current?.stop).toBe("function");
53
+ expect(typeof ref.current?.abort).toBe("function");
54
+ });
55
+
56
+ it("should set default state to idle", () => {
57
+ const ref = createRef<{
58
+ start: () => void;
59
+ stop: () => void;
60
+ abort: () => void;
61
+ state: string;
62
+ }>();
63
+ render(<SpeechRecognition ref={ref as any} />);
64
+ expect(ref.current?.state).toBe("idle");
65
+ });
66
+
67
+ it("should apply custom className", () => {
68
+ const { container } = render(
69
+ <SpeechRecognition className="custom-class" />,
70
+ );
71
+ const root = container.querySelector('[data-slot="speech-recognition"]');
72
+ expect(root?.className).toMatch(/custom-class/);
73
+ });
74
+ });
@@ -0,0 +1 @@
1
+ export { SpeechRecognitionBase as SpeechRecognition } from "./speech-recognition-base";
@@ -0,0 +1,50 @@
1
+ import type { VariantProps } from "class-variance-authority";
2
+ import type { ComponentPropsWithRef, Ref } from "react";
3
+
4
+ import type {
5
+ speechRecognitionBtnVariants,
6
+ speechRecognitionVariants,
7
+ } from "./variants";
8
+
9
+ export type SpeechRecognitionVariantProps = VariantProps<
10
+ typeof speechRecognitionBtnVariants
11
+ >;
12
+
13
+ export type SpeechRecognitionState =
14
+ | "idle"
15
+ | "listening"
16
+ | "processing"
17
+ | "error";
18
+
19
+ export interface SpeechRecognitionResult {
20
+ transcript: string;
21
+ isFinal: boolean;
22
+ }
23
+
24
+ export type SpeechRecognitionBaseProps = VariantProps<
25
+ typeof speechRecognitionVariants
26
+ > &
27
+ VariantProps<typeof speechRecognitionBtnVariants> &
28
+ ComponentPropsWithRef<"div"> & {
29
+ lang?: string;
30
+ continuous?: boolean;
31
+ interimResults?: boolean;
32
+ autoStart?: boolean;
33
+ onResult?: (result: SpeechRecognitionResult) => void;
34
+ onError?: (error: string) => void;
35
+ onStateChange?: (state: SpeechRecognitionState) => void;
36
+ renderMic?: (props: {
37
+ isListening: boolean;
38
+ state: SpeechRecognitionState;
39
+ }) => React.ReactNode;
40
+ children?: React.ReactNode;
41
+ };
42
+
43
+ export type SpeechRecognitionProps = SpeechRecognitionBaseProps;
44
+
45
+ export type SpeechRecognitionRef = {
46
+ start: () => void;
47
+ stop: () => void;
48
+ abort: () => void;
49
+ state: SpeechRecognitionState;
50
+ };