@nucleux/voice-input 0.5.0 → 0.6.0

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/README.md CHANGED
@@ -16,8 +16,28 @@ pnpm add @nucleux/voice-input @nucleux/tokens
16
16
 
17
17
  ```tsx
18
18
  import { VoiceInput } from "@nucleux/voice-input";
19
+
20
+ <VoiceInput
21
+ variant="transcript-stream" // "waveform" (default) | "mic-button" | "transcript-stream"
22
+ state={state} // "idle" | "recording" | "paused" | "transcribing" | "error"
23
+ duration="0:14"
24
+ transcript={finalWords}
25
+ interimTranscript={wordsStillBeingHeard}
26
+ listeningLabel="Aria is listening"
27
+ onStart={start}
28
+ onPause={pause}
29
+ onResume={resume}
30
+ onCancel={discard} // typed text stays
31
+ onConfirm={stop} // puts the transcript in the field — never sends
32
+ onRetry={retry}
33
+ />
19
34
  ```
20
35
 
36
+ Speech recognition is the app's job; this component renders the states. Keep the verbs distinct:
37
+ **Pause** (mic off, keep progress), **Resume**, **Stop** (end, keep what was said, do not send) and
38
+ **Cancel** (discard this recording). State changes are announced through a polite live region, and the
39
+ waveform, caret and pulse stop under `prefers-reduced-motion`.
40
+
21
41
  Requires the theme once in your app: `import "@nucleux/tokens/styles.css";`
22
42
  and the Tailwind preset from `@nucleux/tokens/preset`.
23
43
 
package/dist/index.cjs CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  var React = require('react');
5
5
  var utils = require('@nucleux/utils');
6
+ var button = require('@nucleux/button');
6
7
  var lucideReact = require('lucide-react');
7
8
  var jsxRuntime = require('react/jsx-runtime');
8
9
 
@@ -28,11 +29,14 @@ var React__namespace = /*#__PURE__*/_interopNamespace(React);
28
29
 
29
30
  // src/index.tsx
30
31
  var BAR_COUNT = 24;
31
- function Waveform() {
32
+ function Waveform({ paused = false }) {
32
33
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-8 flex-1 items-center gap-0.5", "aria-hidden": "true", children: Array.from({ length: BAR_COUNT }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(
33
34
  "span",
34
35
  {
35
- className: "w-0.5 flex-1 rounded-full bg-info/70 animate-nx-typing",
36
+ className: utils.cn(
37
+ "w-0.5 flex-1 rounded-full bg-info/70 motion-reduce:animate-none",
38
+ paused ? "opacity-40" : "animate-nx-typing"
39
+ ),
36
40
  style: {
37
41
  height: `${30 + i * 37 % 60}%`,
38
42
  animationDelay: `${i % 6 * 90}ms`
@@ -41,63 +45,210 @@ function Waveform() {
41
45
  i
42
46
  )) });
43
47
  }
48
+ var iconButton = "inline-flex size-9 items-center justify-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&_svg]:size-4";
49
+ var textOf = (node, fallback) => typeof node === "string" || typeof node === "number" ? String(node) : fallback;
44
50
  var VoiceInput = React__namespace.forwardRef(
45
- ({ state = "idle", duration, transcript, onStart, onCancel, onConfirm, className, ...props }, ref) => /* @__PURE__ */ jsxRuntime.jsxs(
46
- "div",
47
- {
48
- ref,
49
- className: utils.cn(
50
- "flex items-center gap-3 rounded-full border border-border bg-background px-3 py-2",
51
- className
51
+ ({
52
+ variant = "waveform",
53
+ state = "idle",
54
+ duration,
55
+ transcript,
56
+ interimTranscript,
57
+ onStart,
58
+ onCancel,
59
+ onConfirm,
60
+ onPause,
61
+ onResume,
62
+ onRetry,
63
+ idleLabel = "Tap to speak",
64
+ listeningLabel = "Listening",
65
+ errorText = "Couldn\u2019t make out all of that.",
66
+ className,
67
+ ...props
68
+ }, ref) => {
69
+ const capturing = state === "recording" || state === "paused";
70
+ const announcement = state === "recording" ? textOf(listeningLabel, "Listening") : state === "paused" ? "Paused" : state === "transcribing" ? "Transcribing" : state === "error" ? textOf(errorText, "Voice input failed") : "";
71
+ const liveRegion = /* @__PURE__ */ jsxRuntime.jsx("span", { role: "status", "aria-live": "polite", className: "sr-only", children: announcement });
72
+ const startButton = /* @__PURE__ */ jsxRuntime.jsx(
73
+ "button",
74
+ {
75
+ type: "button",
76
+ "aria-label": "Start dictation",
77
+ onClick: onStart,
78
+ className: utils.cn(iconButton, "bg-muted text-foreground hover:bg-accent"),
79
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Mic, { "aria-hidden": "true" })
80
+ }
81
+ );
82
+ const cancelButton = /* @__PURE__ */ jsxRuntime.jsx(
83
+ "button",
84
+ {
85
+ type: "button",
86
+ "aria-label": "Cancel dictation",
87
+ onClick: onCancel,
88
+ className: utils.cn(iconButton, "text-muted-foreground hover:bg-accent hover:text-foreground"),
89
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { "aria-hidden": "true" })
90
+ }
91
+ );
92
+ const pauseButton = state === "paused" ? onResume && /* @__PURE__ */ jsxRuntime.jsx(button.Button, { variant: "ghost", size: "icon", "aria-label": "Resume dictation", onClick: onResume, className: "rounded-full", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Play, { className: "size-4", "aria-hidden": "true" }) }) : onPause && /* @__PURE__ */ jsxRuntime.jsx(button.Button, { variant: "ghost", size: "icon", "aria-label": "Pause dictation", onClick: onPause, className: "rounded-full", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Pause, { className: "size-4", "aria-hidden": "true" }) });
93
+ const transcribing = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
94
+ /* @__PURE__ */ jsxRuntime.jsx(
95
+ lucideReact.Loader2,
96
+ {
97
+ className: "size-4 shrink-0 animate-spin text-muted-foreground motion-reduce:animate-none",
98
+ "aria-hidden": "true"
99
+ }
100
+ ),
101
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 truncate text-sm text-muted-foreground", children: transcript ?? "Transcribing\u2026" })
102
+ ] });
103
+ const error = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
104
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertCircle, { className: "size-4 shrink-0 text-destructive", "aria-hidden": "true" }),
105
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 truncate text-sm text-destructive", children: errorText }),
106
+ onRetry && /* @__PURE__ */ jsxRuntime.jsx(
107
+ button.Button,
108
+ {
109
+ variant: "ghost",
110
+ size: "sm",
111
+ leftIcon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RotateCcw, { className: "size-4", "aria-hidden": "true" }),
112
+ onClick: onRetry,
113
+ children: "Retry"
114
+ }
52
115
  ),
53
- role: "group",
54
- "aria-label": "Voice input",
55
- ...props,
56
- children: [
57
- state === "idle" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
58
- /* @__PURE__ */ jsxRuntime.jsx(
59
- "button",
60
- {
61
- type: "button",
62
- "aria-label": "Start dictation",
63
- onClick: onStart,
64
- className: "inline-flex size-9 items-center justify-center rounded-full bg-muted text-foreground transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&_svg]:size-4",
65
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Mic, { "aria-hidden": "true" })
66
- }
67
- ),
68
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 text-sm text-muted-foreground", children: "Tap to speak" })
69
- ] }),
70
- state === "recording" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
71
- /* @__PURE__ */ jsxRuntime.jsx(
72
- "button",
73
- {
74
- type: "button",
75
- "aria-label": "Cancel dictation",
76
- onClick: onCancel,
77
- className: "inline-flex size-9 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&_svg]:size-4",
78
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { "aria-hidden": "true" })
79
- }
80
- ),
81
- /* @__PURE__ */ jsxRuntime.jsx(Waveform, {}),
82
- duration && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm tabular-nums text-muted-foreground", children: duration }),
83
- /* @__PURE__ */ jsxRuntime.jsx(
84
- "button",
85
- {
86
- type: "button",
87
- "aria-label": "Stop and use recording",
88
- onClick: onConfirm,
89
- className: "inline-flex size-9 items-center justify-center rounded-full bg-info text-info-foreground transition-colors hover:bg-info/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&_svg]:size-4",
90
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { "aria-hidden": "true" })
91
- }
92
- )
93
- ] }),
94
- state === "transcribing" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
95
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "size-4 shrink-0 animate-spin text-muted-foreground", "aria-hidden": "true" }),
96
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 truncate text-sm text-muted-foreground", children: transcript ?? "Transcribing\u2026" })
97
- ] })
98
- ]
116
+ onCancel && cancelButton
117
+ ] });
118
+ if (variant === "transcript-stream") {
119
+ return /* @__PURE__ */ jsxRuntime.jsxs(
120
+ "div",
121
+ {
122
+ ref,
123
+ className: utils.cn("flex flex-col gap-2 rounded-xl border border-border bg-background px-4 py-3", className),
124
+ role: "group",
125
+ "aria-label": "Voice input",
126
+ "data-state": state,
127
+ "data-variant": variant,
128
+ ...props,
129
+ children: [
130
+ state === "idle" ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
131
+ startButton,
132
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 text-sm text-muted-foreground", children: idleLabel })
133
+ ] }) : state === "error" ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2", children: error }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
134
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "min-h-5 text-sm leading-5 text-foreground", children: [
135
+ transcript,
136
+ interimTranscript && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
137
+ transcript ? " " : null,
138
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "italic text-muted-foreground", children: interimTranscript })
139
+ ] }),
140
+ capturing && /* @__PURE__ */ jsxRuntime.jsx(
141
+ "span",
142
+ {
143
+ "aria-hidden": "true",
144
+ className: "ml-px inline-block h-3.5 w-0.5 translate-y-0.5 bg-brand animate-nx-caret-blink motion-reduce:animate-none"
145
+ }
146
+ )
147
+ ] }),
148
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2", children: state === "transcribing" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
149
+ /* @__PURE__ */ jsxRuntime.jsx(
150
+ lucideReact.Loader2,
151
+ {
152
+ className: "size-3.5 shrink-0 animate-spin text-muted-foreground motion-reduce:animate-none",
153
+ "aria-hidden": "true"
154
+ }
155
+ ),
156
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 text-xs text-muted-foreground", children: "Transcribing\u2026" })
157
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
158
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Mic, { className: utils.cn("size-3.5 shrink-0", state === "paused" ? "text-muted-foreground" : "text-destructive"), "aria-hidden": "true" }),
159
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex-1 text-xs text-muted-foreground", children: [
160
+ state === "paused" ? "Paused" : listeningLabel,
161
+ duration && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "tabular-nums", children: [
162
+ " \xB7 ",
163
+ duration
164
+ ] })
165
+ ] }),
166
+ pauseButton,
167
+ onCancel && cancelButton,
168
+ /* @__PURE__ */ jsxRuntime.jsx(button.Button, { variant: "cta", size: "sm", onClick: onConfirm, leftIcon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { className: "size-4", "aria-hidden": "true" }), children: "Stop" })
169
+ ] }) })
170
+ ] }),
171
+ liveRegion
172
+ ]
173
+ }
174
+ );
99
175
  }
100
- )
176
+ return /* @__PURE__ */ jsxRuntime.jsxs(
177
+ "div",
178
+ {
179
+ ref,
180
+ className: utils.cn(
181
+ "flex items-center gap-3 rounded-full border border-border bg-background px-3 py-2",
182
+ className
183
+ ),
184
+ role: "group",
185
+ "aria-label": "Voice input",
186
+ "data-state": state,
187
+ "data-variant": variant,
188
+ ...props,
189
+ children: [
190
+ state === "idle" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
191
+ startButton,
192
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 text-sm text-muted-foreground", children: idleLabel })
193
+ ] }),
194
+ capturing && variant === "waveform" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
195
+ cancelButton,
196
+ /* @__PURE__ */ jsxRuntime.jsx(Waveform, { paused: state === "paused" }),
197
+ duration && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm tabular-nums text-muted-foreground", children: duration }),
198
+ pauseButton,
199
+ /* @__PURE__ */ jsxRuntime.jsx(
200
+ "button",
201
+ {
202
+ type: "button",
203
+ "aria-label": "Stop and use recording",
204
+ onClick: onConfirm,
205
+ className: utils.cn(iconButton, "bg-info text-info-foreground hover:bg-info/90"),
206
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { "aria-hidden": "true" })
207
+ }
208
+ )
209
+ ] }),
210
+ capturing && variant === "mic-button" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
211
+ cancelButton,
212
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex flex-1 items-center gap-2 text-sm text-foreground", children: [
213
+ /* @__PURE__ */ jsxRuntime.jsx(
214
+ "span",
215
+ {
216
+ "aria-hidden": "true",
217
+ className: utils.cn(
218
+ "size-2 rounded-full bg-destructive",
219
+ state === "recording" ? "animate-pulse motion-reduce:animate-none" : "opacity-40"
220
+ )
221
+ }
222
+ ),
223
+ state === "paused" ? "Paused" : listeningLabel,
224
+ duration && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "tabular-nums text-muted-foreground", children: [
225
+ "\xB7 ",
226
+ duration
227
+ ] })
228
+ ] }),
229
+ pauseButton,
230
+ /* @__PURE__ */ jsxRuntime.jsx(
231
+ "button",
232
+ {
233
+ type: "button",
234
+ "aria-label": "Stop and use recording",
235
+ onClick: onConfirm,
236
+ className: utils.cn(
237
+ iconButton,
238
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
239
+ state === "recording" && "animate-pulse motion-reduce:animate-none"
240
+ ),
241
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Square, { className: "fill-current", "aria-hidden": "true" })
242
+ }
243
+ )
244
+ ] }),
245
+ state === "transcribing" && transcribing,
246
+ state === "error" && error,
247
+ liveRegion
248
+ ]
249
+ }
250
+ );
251
+ }
101
252
  );
102
253
  VoiceInput.displayName = "VoiceInput";
103
254
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.tsx"],"names":["jsx","React","jsxs","cn","Fragment","Mic","X","Check","Loader2"],"mappings":"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","file":"index.cjs","sourcesContent":["import * as React from \"react\";\nimport { cn } from \"@nucleux/utils\";\nimport { Mic, X, Check, Loader2 } from \"lucide-react\";\n\nexport type VoiceInputState = \"idle\" | \"recording\" | \"transcribing\";\n\nexport interface VoiceInputProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Current state of the dictation flow. */\n state?: VoiceInputState;\n /** Elapsed time label while recording, e.g. `\"0:12\"`. */\n duration?: React.ReactNode;\n /** Transcribed text shown once available. */\n transcript?: React.ReactNode;\n /** Called from the idle mic button to start recording. */\n onStart?: () => void;\n /** Called to cancel/discard the recording. */\n onCancel?: () => void;\n /** Called to stop recording and confirm. */\n onConfirm?: () => void;\n}\n\nconst BAR_COUNT = 24;\n\nfunction Waveform() {\n return (\n <div className=\"flex h-8 flex-1 items-center gap-0.5\" aria-hidden=\"true\">\n {Array.from({ length: BAR_COUNT }).map((_, i) => (\n <span\n key={i}\n className=\"w-0.5 flex-1 rounded-full bg-info/70 animate-nx-typing\"\n style={{\n height: `${30 + ((i * 37) % 60)}%`,\n animationDelay: `${(i % 6) * 90}ms`,\n }}\n />\n ))}\n </div>\n );\n}\n\n/**\n * A composer in a live-dictation state — mic trigger, animated waveform while\n * recording, and a transcribing state — for voice input to the agent.\n * Source: Figma \"Expressive Input / Voice Input\".\n */\nexport const VoiceInput = React.forwardRef<HTMLDivElement, VoiceInputProps>(\n ({ state = \"idle\", duration, transcript, onStart, onCancel, onConfirm, className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\n \"flex items-center gap-3 rounded-full border border-border bg-background px-3 py-2\",\n className,\n )}\n role=\"group\"\n aria-label=\"Voice input\"\n {...props}\n >\n {state === \"idle\" && (\n <>\n <button\n type=\"button\"\n aria-label=\"Start dictation\"\n onClick={onStart}\n className=\"inline-flex size-9 items-center justify-center rounded-full bg-muted text-foreground transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&_svg]:size-4\"\n >\n <Mic aria-hidden=\"true\" />\n </button>\n <span className=\"flex-1 text-sm text-muted-foreground\">Tap to speak</span>\n </>\n )}\n\n {state === \"recording\" && (\n <>\n <button\n type=\"button\"\n aria-label=\"Cancel dictation\"\n onClick={onCancel}\n className=\"inline-flex size-9 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&_svg]:size-4\"\n >\n <X aria-hidden=\"true\" />\n </button>\n <Waveform />\n {duration && <span className=\"text-sm tabular-nums text-muted-foreground\">{duration}</span>}\n <button\n type=\"button\"\n aria-label=\"Stop and use recording\"\n onClick={onConfirm}\n className=\"inline-flex size-9 items-center justify-center rounded-full bg-info text-info-foreground transition-colors hover:bg-info/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&_svg]:size-4\"\n >\n <Check aria-hidden=\"true\" />\n </button>\n </>\n )}\n\n {state === \"transcribing\" && (\n <>\n <Loader2 className=\"size-4 shrink-0 animate-spin text-muted-foreground\" aria-hidden=\"true\" />\n <span className=\"flex-1 truncate text-sm text-muted-foreground\">\n {transcript ?? \"Transcribing…\"}\n </span>\n </>\n )}\n </div>\n ),\n);\nVoiceInput.displayName = \"VoiceInput\";\n"]}
1
+ {"version":3,"sources":["../src/index.tsx"],"names":["jsx","cn","React","Mic","X","Button","Play","Pause","jsxs","Fragment","Loader2","AlertCircle","RotateCcw","Check","Square"],"mappings":"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","file":"index.cjs","sourcesContent":["import * as React from \"react\";\nimport { cn } from \"@nucleux/utils\";\nimport { Button } from \"@nucleux/button\";\nimport { AlertCircle, Check, Loader2, Mic, Pause, Play, RotateCcw, Square, X } from \"lucide-react\";\n\n/**\n * `\"idle\"` → `\"recording\"` (⇄ `\"paused\"`) → `\"transcribing\"`; `\"error\"` when the speech\n * could not be captured or trusted.\n */\nexport type VoiceInputState = \"idle\" | \"recording\" | \"paused\" | \"transcribing\" | \"error\";\n\n/**\n * `\"waveform\"` (default) — the dictation bar with a live waveform;\n * `\"mic-button\"` — a compact tap-to-talk control with a clear stop state;\n * `\"transcript-stream\"` — a panel where the words appear as they are heard.\n */\nexport type VoiceInputVariant = \"waveform\" | \"mic-button\" | \"transcript-stream\";\n\nexport interface VoiceInputProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Presentation. Defaults to `\"waveform\"`. */\n variant?: VoiceInputVariant;\n /** Current state of the dictation flow. */\n state?: VoiceInputState;\n /** Elapsed time label while recording, e.g. `\"0:12\"`. */\n duration?: React.ReactNode;\n /** Transcribed text: the final words (shown while recording in `\"transcript-stream\"`, and once transcribing). */\n transcript?: React.ReactNode;\n /** Words still being heard (not final) — shown muted after `transcript`, with a caret. */\n interimTranscript?: React.ReactNode;\n /** Called from the idle mic button to start recording. */\n onStart?: () => void;\n /** Called to cancel/discard the recording. Typed text is untouched. */\n onCancel?: () => void;\n /** Called to stop recording and confirm. The transcript goes to the field for review — it never sends. */\n onConfirm?: () => void;\n /** Called to pause capture (mic off, progress kept). Renders a Pause control while recording. */\n onPause?: () => void;\n /** Called to resume a paused recording. */\n onResume?: () => void;\n /** Called from the error state to try again. */\n onRetry?: () => void;\n /** Label shown while idle. Defaults to `\"Tap to speak\"`. */\n idleLabel?: React.ReactNode;\n /** Label shown while capturing, e.g. `\"Aria is listening\"`. Defaults to `\"Listening\"`. */\n listeningLabel?: React.ReactNode;\n /** Message shown in the error state. Defaults to `\"Couldn’t make out all of that.\"`. */\n errorText?: React.ReactNode;\n}\n\nconst BAR_COUNT = 24;\n\nfunction Waveform({ paused = false }: { paused?: boolean }) {\n return (\n <div className=\"flex h-8 flex-1 items-center gap-0.5\" aria-hidden=\"true\">\n {Array.from({ length: BAR_COUNT }).map((_, i) => (\n <span\n key={i}\n className={cn(\n \"w-0.5 flex-1 rounded-full bg-info/70 motion-reduce:animate-none\",\n paused ? \"opacity-40\" : \"animate-nx-typing\",\n )}\n style={{\n height: `${30 + ((i * 37) % 60)}%`,\n animationDelay: `${(i % 6) * 90}ms`,\n }}\n />\n ))}\n </div>\n );\n}\n\nconst iconButton =\n \"inline-flex size-9 items-center justify-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&_svg]:size-4\";\n\nconst textOf = (node: React.ReactNode, fallback: string) =>\n typeof node === \"string\" || typeof node === \"number\" ? String(node) : fallback;\n\n/**\n * Voice input: speech as an input method into the composer — a mic trigger,\n * a clearly lit capturing state (waveform or pulsing stop button, clock, live\n * words), Pause/Resume, Cancel and Stop, then a transcribing state. Stop never\n * sends: the transcript goes to the field for the person to review. State is\n * announced through a polite live region and every animation stops under\n * reduced motion. Variants: waveform, mic-button, transcript-stream.\n * Source: Figma \"Expressive Input / Voice Input\".\n */\nexport const VoiceInput = React.forwardRef<HTMLDivElement, VoiceInputProps>(\n (\n {\n variant = \"waveform\",\n state = \"idle\",\n duration,\n transcript,\n interimTranscript,\n onStart,\n onCancel,\n onConfirm,\n onPause,\n onResume,\n onRetry,\n idleLabel = \"Tap to speak\",\n listeningLabel = \"Listening\",\n errorText = \"Couldn’t make out all of that.\",\n className,\n ...props\n },\n ref,\n ) => {\n const capturing = state === \"recording\" || state === \"paused\";\n const announcement =\n state === \"recording\"\n ? textOf(listeningLabel, \"Listening\")\n : state === \"paused\"\n ? \"Paused\"\n : state === \"transcribing\"\n ? \"Transcribing\"\n : state === \"error\"\n ? textOf(errorText, \"Voice input failed\")\n : \"\";\n\n const liveRegion = (\n <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {announcement}\n </span>\n );\n\n const startButton = (\n <button\n type=\"button\"\n aria-label=\"Start dictation\"\n onClick={onStart}\n className={cn(iconButton, \"bg-muted text-foreground hover:bg-accent\")}\n >\n <Mic aria-hidden=\"true\" />\n </button>\n );\n\n const cancelButton = (\n <button\n type=\"button\"\n aria-label=\"Cancel dictation\"\n onClick={onCancel}\n className={cn(iconButton, \"text-muted-foreground hover:bg-accent hover:text-foreground\")}\n >\n <X aria-hidden=\"true\" />\n </button>\n );\n\n const pauseButton =\n state === \"paused\"\n ? onResume && (\n <Button variant=\"ghost\" size=\"icon\" aria-label=\"Resume dictation\" onClick={onResume} className=\"rounded-full\">\n <Play className=\"size-4\" aria-hidden=\"true\" />\n </Button>\n )\n : onPause && (\n <Button variant=\"ghost\" size=\"icon\" aria-label=\"Pause dictation\" onClick={onPause} className=\"rounded-full\">\n <Pause className=\"size-4\" aria-hidden=\"true\" />\n </Button>\n );\n\n const transcribing = (\n <>\n <Loader2\n className=\"size-4 shrink-0 animate-spin text-muted-foreground motion-reduce:animate-none\"\n aria-hidden=\"true\"\n />\n <span className=\"flex-1 truncate text-sm text-muted-foreground\">{transcript ?? \"Transcribing…\"}</span>\n </>\n );\n\n const error = (\n <>\n <AlertCircle className=\"size-4 shrink-0 text-destructive\" aria-hidden=\"true\" />\n <span className=\"flex-1 truncate text-sm text-destructive\">{errorText}</span>\n {onRetry && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n leftIcon={<RotateCcw className=\"size-4\" aria-hidden=\"true\" />}\n onClick={onRetry}\n >\n Retry\n </Button>\n )}\n {onCancel && cancelButton}\n </>\n );\n\n if (variant === \"transcript-stream\") {\n return (\n <div\n ref={ref}\n className={cn(\"flex flex-col gap-2 rounded-xl border border-border bg-background px-4 py-3\", className)}\n role=\"group\"\n aria-label=\"Voice input\"\n data-state={state}\n data-variant={variant}\n {...props}\n >\n {state === \"idle\" ? (\n <div className=\"flex items-center gap-3\">\n {startButton}\n <span className=\"flex-1 text-sm text-muted-foreground\">{idleLabel}</span>\n </div>\n ) : state === \"error\" ? (\n <div className=\"flex items-center gap-2\">{error}</div>\n ) : (\n <>\n <p className=\"min-h-5 text-sm leading-5 text-foreground\">\n {transcript}\n {interimTranscript && (\n <>\n {transcript ? \" \" : null}\n <span className=\"italic text-muted-foreground\">{interimTranscript}</span>\n </>\n )}\n {capturing && (\n <span\n aria-hidden=\"true\"\n className=\"ml-px inline-block h-3.5 w-0.5 translate-y-0.5 bg-brand animate-nx-caret-blink motion-reduce:animate-none\"\n />\n )}\n </p>\n <div className=\"flex items-center gap-2\">\n {state === \"transcribing\" ? (\n <>\n <Loader2\n className=\"size-3.5 shrink-0 animate-spin text-muted-foreground motion-reduce:animate-none\"\n aria-hidden=\"true\"\n />\n <span className=\"flex-1 text-xs text-muted-foreground\">Transcribing…</span>\n </>\n ) : (\n <>\n <Mic className={cn(\"size-3.5 shrink-0\", state === \"paused\" ? \"text-muted-foreground\" : \"text-destructive\")} aria-hidden=\"true\" />\n <span className=\"flex-1 text-xs text-muted-foreground\">\n {state === \"paused\" ? \"Paused\" : listeningLabel}\n {duration && <span className=\"tabular-nums\"> · {duration}</span>}\n </span>\n {pauseButton}\n {onCancel && cancelButton}\n <Button variant=\"cta\" size=\"sm\" onClick={onConfirm} leftIcon={<Check className=\"size-4\" aria-hidden=\"true\" />}>\n Stop\n </Button>\n </>\n )}\n </div>\n </>\n )}\n {liveRegion}\n </div>\n );\n }\n\n return (\n <div\n ref={ref}\n className={cn(\n \"flex items-center gap-3 rounded-full border border-border bg-background px-3 py-2\",\n className,\n )}\n role=\"group\"\n aria-label=\"Voice input\"\n data-state={state}\n data-variant={variant}\n {...props}\n >\n {state === \"idle\" && (\n <>\n {startButton}\n <span className=\"flex-1 text-sm text-muted-foreground\">{idleLabel}</span>\n </>\n )}\n\n {capturing && variant === \"waveform\" && (\n <>\n {cancelButton}\n <Waveform paused={state === \"paused\"} />\n {duration && <span className=\"text-sm tabular-nums text-muted-foreground\">{duration}</span>}\n {pauseButton}\n <button\n type=\"button\"\n aria-label=\"Stop and use recording\"\n onClick={onConfirm}\n className={cn(iconButton, \"bg-info text-info-foreground hover:bg-info/90\")}\n >\n <Check aria-hidden=\"true\" />\n </button>\n </>\n )}\n\n {capturing && variant === \"mic-button\" && (\n <>\n {cancelButton}\n <span className=\"flex flex-1 items-center gap-2 text-sm text-foreground\">\n <span\n aria-hidden=\"true\"\n className={cn(\n \"size-2 rounded-full bg-destructive\",\n state === \"recording\" ? \"animate-pulse motion-reduce:animate-none\" : \"opacity-40\",\n )}\n />\n {state === \"paused\" ? \"Paused\" : listeningLabel}\n {duration && <span className=\"tabular-nums text-muted-foreground\">· {duration}</span>}\n </span>\n {pauseButton}\n <button\n type=\"button\"\n aria-label=\"Stop and use recording\"\n onClick={onConfirm}\n className={cn(\n iconButton,\n \"bg-destructive text-destructive-foreground hover:bg-destructive/90\",\n state === \"recording\" && \"animate-pulse motion-reduce:animate-none\",\n )}\n >\n <Square className=\"fill-current\" aria-hidden=\"true\" />\n </button>\n </>\n )}\n\n {state === \"transcribing\" && transcribing}\n {state === \"error\" && error}\n {liveRegion}\n </div>\n );\n },\n);\nVoiceInput.displayName = \"VoiceInput\";\n"]}
package/dist/index.d.cts CHANGED
@@ -1,25 +1,55 @@
1
1
  import * as React from 'react';
2
2
 
3
- type VoiceInputState = "idle" | "recording" | "transcribing";
3
+ /**
4
+ * `"idle"` → `"recording"` (⇄ `"paused"`) → `"transcribing"`; `"error"` when the speech
5
+ * could not be captured or trusted.
6
+ */
7
+ type VoiceInputState = "idle" | "recording" | "paused" | "transcribing" | "error";
8
+ /**
9
+ * `"waveform"` (default) — the dictation bar with a live waveform;
10
+ * `"mic-button"` — a compact tap-to-talk control with a clear stop state;
11
+ * `"transcript-stream"` — a panel where the words appear as they are heard.
12
+ */
13
+ type VoiceInputVariant = "waveform" | "mic-button" | "transcript-stream";
4
14
  interface VoiceInputProps extends React.HTMLAttributes<HTMLDivElement> {
15
+ /** Presentation. Defaults to `"waveform"`. */
16
+ variant?: VoiceInputVariant;
5
17
  /** Current state of the dictation flow. */
6
18
  state?: VoiceInputState;
7
19
  /** Elapsed time label while recording, e.g. `"0:12"`. */
8
20
  duration?: React.ReactNode;
9
- /** Transcribed text shown once available. */
21
+ /** Transcribed text: the final words (shown while recording in `"transcript-stream"`, and once transcribing). */
10
22
  transcript?: React.ReactNode;
23
+ /** Words still being heard (not final) — shown muted after `transcript`, with a caret. */
24
+ interimTranscript?: React.ReactNode;
11
25
  /** Called from the idle mic button to start recording. */
12
26
  onStart?: () => void;
13
- /** Called to cancel/discard the recording. */
27
+ /** Called to cancel/discard the recording. Typed text is untouched. */
14
28
  onCancel?: () => void;
15
- /** Called to stop recording and confirm. */
29
+ /** Called to stop recording and confirm. The transcript goes to the field for review — it never sends. */
16
30
  onConfirm?: () => void;
31
+ /** Called to pause capture (mic off, progress kept). Renders a Pause control while recording. */
32
+ onPause?: () => void;
33
+ /** Called to resume a paused recording. */
34
+ onResume?: () => void;
35
+ /** Called from the error state to try again. */
36
+ onRetry?: () => void;
37
+ /** Label shown while idle. Defaults to `"Tap to speak"`. */
38
+ idleLabel?: React.ReactNode;
39
+ /** Label shown while capturing, e.g. `"Aria is listening"`. Defaults to `"Listening"`. */
40
+ listeningLabel?: React.ReactNode;
41
+ /** Message shown in the error state. Defaults to `"Couldn’t make out all of that."`. */
42
+ errorText?: React.ReactNode;
17
43
  }
18
44
  /**
19
- * A composer in a live-dictation state — mic trigger, animated waveform while
20
- * recording, and a transcribing state — for voice input to the agent.
45
+ * Voice input: speech as an input method into the composer — a mic trigger,
46
+ * a clearly lit capturing state (waveform or pulsing stop button, clock, live
47
+ * words), Pause/Resume, Cancel and Stop, then a transcribing state. Stop never
48
+ * sends: the transcript goes to the field for the person to review. State is
49
+ * announced through a polite live region and every animation stops under
50
+ * reduced motion. Variants: waveform, mic-button, transcript-stream.
21
51
  * Source: Figma "Expressive Input / Voice Input".
22
52
  */
23
53
  declare const VoiceInput: React.ForwardRefExoticComponent<VoiceInputProps & React.RefAttributes<HTMLDivElement>>;
24
54
 
25
- export { VoiceInput, type VoiceInputProps, type VoiceInputState };
55
+ export { VoiceInput, type VoiceInputProps, type VoiceInputState, type VoiceInputVariant };
package/dist/index.d.ts CHANGED
@@ -1,25 +1,55 @@
1
1
  import * as React from 'react';
2
2
 
3
- type VoiceInputState = "idle" | "recording" | "transcribing";
3
+ /**
4
+ * `"idle"` → `"recording"` (⇄ `"paused"`) → `"transcribing"`; `"error"` when the speech
5
+ * could not be captured or trusted.
6
+ */
7
+ type VoiceInputState = "idle" | "recording" | "paused" | "transcribing" | "error";
8
+ /**
9
+ * `"waveform"` (default) — the dictation bar with a live waveform;
10
+ * `"mic-button"` — a compact tap-to-talk control with a clear stop state;
11
+ * `"transcript-stream"` — a panel where the words appear as they are heard.
12
+ */
13
+ type VoiceInputVariant = "waveform" | "mic-button" | "transcript-stream";
4
14
  interface VoiceInputProps extends React.HTMLAttributes<HTMLDivElement> {
15
+ /** Presentation. Defaults to `"waveform"`. */
16
+ variant?: VoiceInputVariant;
5
17
  /** Current state of the dictation flow. */
6
18
  state?: VoiceInputState;
7
19
  /** Elapsed time label while recording, e.g. `"0:12"`. */
8
20
  duration?: React.ReactNode;
9
- /** Transcribed text shown once available. */
21
+ /** Transcribed text: the final words (shown while recording in `"transcript-stream"`, and once transcribing). */
10
22
  transcript?: React.ReactNode;
23
+ /** Words still being heard (not final) — shown muted after `transcript`, with a caret. */
24
+ interimTranscript?: React.ReactNode;
11
25
  /** Called from the idle mic button to start recording. */
12
26
  onStart?: () => void;
13
- /** Called to cancel/discard the recording. */
27
+ /** Called to cancel/discard the recording. Typed text is untouched. */
14
28
  onCancel?: () => void;
15
- /** Called to stop recording and confirm. */
29
+ /** Called to stop recording and confirm. The transcript goes to the field for review — it never sends. */
16
30
  onConfirm?: () => void;
31
+ /** Called to pause capture (mic off, progress kept). Renders a Pause control while recording. */
32
+ onPause?: () => void;
33
+ /** Called to resume a paused recording. */
34
+ onResume?: () => void;
35
+ /** Called from the error state to try again. */
36
+ onRetry?: () => void;
37
+ /** Label shown while idle. Defaults to `"Tap to speak"`. */
38
+ idleLabel?: React.ReactNode;
39
+ /** Label shown while capturing, e.g. `"Aria is listening"`. Defaults to `"Listening"`. */
40
+ listeningLabel?: React.ReactNode;
41
+ /** Message shown in the error state. Defaults to `"Couldn’t make out all of that."`. */
42
+ errorText?: React.ReactNode;
17
43
  }
18
44
  /**
19
- * A composer in a live-dictation state — mic trigger, animated waveform while
20
- * recording, and a transcribing state — for voice input to the agent.
45
+ * Voice input: speech as an input method into the composer — a mic trigger,
46
+ * a clearly lit capturing state (waveform or pulsing stop button, clock, live
47
+ * words), Pause/Resume, Cancel and Stop, then a transcribing state. Stop never
48
+ * sends: the transcript goes to the field for the person to review. State is
49
+ * announced through a polite live region and every animation stops under
50
+ * reduced motion. Variants: waveform, mic-button, transcript-stream.
21
51
  * Source: Figma "Expressive Input / Voice Input".
22
52
  */
23
53
  declare const VoiceInput: React.ForwardRefExoticComponent<VoiceInputProps & React.RefAttributes<HTMLDivElement>>;
24
54
 
25
- export { VoiceInput, type VoiceInputProps, type VoiceInputState };
55
+ export { VoiceInput, type VoiceInputProps, type VoiceInputState, type VoiceInputVariant };
package/dist/index.js CHANGED
@@ -1,16 +1,20 @@
1
1
  "use client";
2
2
  import * as React from 'react';
3
3
  import { cn } from '@nucleux/utils';
4
- import { Mic, X, Check, Loader2 } from 'lucide-react';
5
- import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
4
+ import { Button } from '@nucleux/button';
5
+ import { Mic, X, Loader2, Check, Square, AlertCircle, RotateCcw, Play, Pause } from 'lucide-react';
6
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
7
 
7
8
  // src/index.tsx
8
9
  var BAR_COUNT = 24;
9
- function Waveform() {
10
+ function Waveform({ paused = false }) {
10
11
  return /* @__PURE__ */ jsx("div", { className: "flex h-8 flex-1 items-center gap-0.5", "aria-hidden": "true", children: Array.from({ length: BAR_COUNT }).map((_, i) => /* @__PURE__ */ jsx(
11
12
  "span",
12
13
  {
13
- className: "w-0.5 flex-1 rounded-full bg-info/70 animate-nx-typing",
14
+ className: cn(
15
+ "w-0.5 flex-1 rounded-full bg-info/70 motion-reduce:animate-none",
16
+ paused ? "opacity-40" : "animate-nx-typing"
17
+ ),
14
18
  style: {
15
19
  height: `${30 + i * 37 % 60}%`,
16
20
  animationDelay: `${i % 6 * 90}ms`
@@ -19,63 +23,210 @@ function Waveform() {
19
23
  i
20
24
  )) });
21
25
  }
26
+ var iconButton = "inline-flex size-9 items-center justify-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&_svg]:size-4";
27
+ var textOf = (node, fallback) => typeof node === "string" || typeof node === "number" ? String(node) : fallback;
22
28
  var VoiceInput = React.forwardRef(
23
- ({ state = "idle", duration, transcript, onStart, onCancel, onConfirm, className, ...props }, ref) => /* @__PURE__ */ jsxs(
24
- "div",
25
- {
26
- ref,
27
- className: cn(
28
- "flex items-center gap-3 rounded-full border border-border bg-background px-3 py-2",
29
- className
29
+ ({
30
+ variant = "waveform",
31
+ state = "idle",
32
+ duration,
33
+ transcript,
34
+ interimTranscript,
35
+ onStart,
36
+ onCancel,
37
+ onConfirm,
38
+ onPause,
39
+ onResume,
40
+ onRetry,
41
+ idleLabel = "Tap to speak",
42
+ listeningLabel = "Listening",
43
+ errorText = "Couldn\u2019t make out all of that.",
44
+ className,
45
+ ...props
46
+ }, ref) => {
47
+ const capturing = state === "recording" || state === "paused";
48
+ const announcement = state === "recording" ? textOf(listeningLabel, "Listening") : state === "paused" ? "Paused" : state === "transcribing" ? "Transcribing" : state === "error" ? textOf(errorText, "Voice input failed") : "";
49
+ const liveRegion = /* @__PURE__ */ jsx("span", { role: "status", "aria-live": "polite", className: "sr-only", children: announcement });
50
+ const startButton = /* @__PURE__ */ jsx(
51
+ "button",
52
+ {
53
+ type: "button",
54
+ "aria-label": "Start dictation",
55
+ onClick: onStart,
56
+ className: cn(iconButton, "bg-muted text-foreground hover:bg-accent"),
57
+ children: /* @__PURE__ */ jsx(Mic, { "aria-hidden": "true" })
58
+ }
59
+ );
60
+ const cancelButton = /* @__PURE__ */ jsx(
61
+ "button",
62
+ {
63
+ type: "button",
64
+ "aria-label": "Cancel dictation",
65
+ onClick: onCancel,
66
+ className: cn(iconButton, "text-muted-foreground hover:bg-accent hover:text-foreground"),
67
+ children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true" })
68
+ }
69
+ );
70
+ const pauseButton = state === "paused" ? onResume && /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", "aria-label": "Resume dictation", onClick: onResume, className: "rounded-full", children: /* @__PURE__ */ jsx(Play, { className: "size-4", "aria-hidden": "true" }) }) : onPause && /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", "aria-label": "Pause dictation", onClick: onPause, className: "rounded-full", children: /* @__PURE__ */ jsx(Pause, { className: "size-4", "aria-hidden": "true" }) });
71
+ const transcribing = /* @__PURE__ */ jsxs(Fragment, { children: [
72
+ /* @__PURE__ */ jsx(
73
+ Loader2,
74
+ {
75
+ className: "size-4 shrink-0 animate-spin text-muted-foreground motion-reduce:animate-none",
76
+ "aria-hidden": "true"
77
+ }
30
78
  ),
31
- role: "group",
32
- "aria-label": "Voice input",
33
- ...props,
34
- children: [
35
- state === "idle" && /* @__PURE__ */ jsxs(Fragment, { children: [
36
- /* @__PURE__ */ jsx(
37
- "button",
38
- {
39
- type: "button",
40
- "aria-label": "Start dictation",
41
- onClick: onStart,
42
- className: "inline-flex size-9 items-center justify-center rounded-full bg-muted text-foreground transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&_svg]:size-4",
43
- children: /* @__PURE__ */ jsx(Mic, { "aria-hidden": "true" })
44
- }
45
- ),
46
- /* @__PURE__ */ jsx("span", { className: "flex-1 text-sm text-muted-foreground", children: "Tap to speak" })
47
- ] }),
48
- state === "recording" && /* @__PURE__ */ jsxs(Fragment, { children: [
49
- /* @__PURE__ */ jsx(
50
- "button",
51
- {
52
- type: "button",
53
- "aria-label": "Cancel dictation",
54
- onClick: onCancel,
55
- className: "inline-flex size-9 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&_svg]:size-4",
56
- children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true" })
57
- }
58
- ),
59
- /* @__PURE__ */ jsx(Waveform, {}),
60
- duration && /* @__PURE__ */ jsx("span", { className: "text-sm tabular-nums text-muted-foreground", children: duration }),
61
- /* @__PURE__ */ jsx(
62
- "button",
63
- {
64
- type: "button",
65
- "aria-label": "Stop and use recording",
66
- onClick: onConfirm,
67
- className: "inline-flex size-9 items-center justify-center rounded-full bg-info text-info-foreground transition-colors hover:bg-info/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&_svg]:size-4",
68
- children: /* @__PURE__ */ jsx(Check, { "aria-hidden": "true" })
69
- }
70
- )
71
- ] }),
72
- state === "transcribing" && /* @__PURE__ */ jsxs(Fragment, { children: [
73
- /* @__PURE__ */ jsx(Loader2, { className: "size-4 shrink-0 animate-spin text-muted-foreground", "aria-hidden": "true" }),
74
- /* @__PURE__ */ jsx("span", { className: "flex-1 truncate text-sm text-muted-foreground", children: transcript ?? "Transcribing\u2026" })
75
- ] })
76
- ]
79
+ /* @__PURE__ */ jsx("span", { className: "flex-1 truncate text-sm text-muted-foreground", children: transcript ?? "Transcribing\u2026" })
80
+ ] });
81
+ const error = /* @__PURE__ */ jsxs(Fragment, { children: [
82
+ /* @__PURE__ */ jsx(AlertCircle, { className: "size-4 shrink-0 text-destructive", "aria-hidden": "true" }),
83
+ /* @__PURE__ */ jsx("span", { className: "flex-1 truncate text-sm text-destructive", children: errorText }),
84
+ onRetry && /* @__PURE__ */ jsx(
85
+ Button,
86
+ {
87
+ variant: "ghost",
88
+ size: "sm",
89
+ leftIcon: /* @__PURE__ */ jsx(RotateCcw, { className: "size-4", "aria-hidden": "true" }),
90
+ onClick: onRetry,
91
+ children: "Retry"
92
+ }
93
+ ),
94
+ onCancel && cancelButton
95
+ ] });
96
+ if (variant === "transcript-stream") {
97
+ return /* @__PURE__ */ jsxs(
98
+ "div",
99
+ {
100
+ ref,
101
+ className: cn("flex flex-col gap-2 rounded-xl border border-border bg-background px-4 py-3", className),
102
+ role: "group",
103
+ "aria-label": "Voice input",
104
+ "data-state": state,
105
+ "data-variant": variant,
106
+ ...props,
107
+ children: [
108
+ state === "idle" ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
109
+ startButton,
110
+ /* @__PURE__ */ jsx("span", { className: "flex-1 text-sm text-muted-foreground", children: idleLabel })
111
+ ] }) : state === "error" ? /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: error }) : /* @__PURE__ */ jsxs(Fragment, { children: [
112
+ /* @__PURE__ */ jsxs("p", { className: "min-h-5 text-sm leading-5 text-foreground", children: [
113
+ transcript,
114
+ interimTranscript && /* @__PURE__ */ jsxs(Fragment, { children: [
115
+ transcript ? " " : null,
116
+ /* @__PURE__ */ jsx("span", { className: "italic text-muted-foreground", children: interimTranscript })
117
+ ] }),
118
+ capturing && /* @__PURE__ */ jsx(
119
+ "span",
120
+ {
121
+ "aria-hidden": "true",
122
+ className: "ml-px inline-block h-3.5 w-0.5 translate-y-0.5 bg-brand animate-nx-caret-blink motion-reduce:animate-none"
123
+ }
124
+ )
125
+ ] }),
126
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: state === "transcribing" ? /* @__PURE__ */ jsxs(Fragment, { children: [
127
+ /* @__PURE__ */ jsx(
128
+ Loader2,
129
+ {
130
+ className: "size-3.5 shrink-0 animate-spin text-muted-foreground motion-reduce:animate-none",
131
+ "aria-hidden": "true"
132
+ }
133
+ ),
134
+ /* @__PURE__ */ jsx("span", { className: "flex-1 text-xs text-muted-foreground", children: "Transcribing\u2026" })
135
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
136
+ /* @__PURE__ */ jsx(Mic, { className: cn("size-3.5 shrink-0", state === "paused" ? "text-muted-foreground" : "text-destructive"), "aria-hidden": "true" }),
137
+ /* @__PURE__ */ jsxs("span", { className: "flex-1 text-xs text-muted-foreground", children: [
138
+ state === "paused" ? "Paused" : listeningLabel,
139
+ duration && /* @__PURE__ */ jsxs("span", { className: "tabular-nums", children: [
140
+ " \xB7 ",
141
+ duration
142
+ ] })
143
+ ] }),
144
+ pauseButton,
145
+ onCancel && cancelButton,
146
+ /* @__PURE__ */ jsx(Button, { variant: "cta", size: "sm", onClick: onConfirm, leftIcon: /* @__PURE__ */ jsx(Check, { className: "size-4", "aria-hidden": "true" }), children: "Stop" })
147
+ ] }) })
148
+ ] }),
149
+ liveRegion
150
+ ]
151
+ }
152
+ );
77
153
  }
78
- )
154
+ return /* @__PURE__ */ jsxs(
155
+ "div",
156
+ {
157
+ ref,
158
+ className: cn(
159
+ "flex items-center gap-3 rounded-full border border-border bg-background px-3 py-2",
160
+ className
161
+ ),
162
+ role: "group",
163
+ "aria-label": "Voice input",
164
+ "data-state": state,
165
+ "data-variant": variant,
166
+ ...props,
167
+ children: [
168
+ state === "idle" && /* @__PURE__ */ jsxs(Fragment, { children: [
169
+ startButton,
170
+ /* @__PURE__ */ jsx("span", { className: "flex-1 text-sm text-muted-foreground", children: idleLabel })
171
+ ] }),
172
+ capturing && variant === "waveform" && /* @__PURE__ */ jsxs(Fragment, { children: [
173
+ cancelButton,
174
+ /* @__PURE__ */ jsx(Waveform, { paused: state === "paused" }),
175
+ duration && /* @__PURE__ */ jsx("span", { className: "text-sm tabular-nums text-muted-foreground", children: duration }),
176
+ pauseButton,
177
+ /* @__PURE__ */ jsx(
178
+ "button",
179
+ {
180
+ type: "button",
181
+ "aria-label": "Stop and use recording",
182
+ onClick: onConfirm,
183
+ className: cn(iconButton, "bg-info text-info-foreground hover:bg-info/90"),
184
+ children: /* @__PURE__ */ jsx(Check, { "aria-hidden": "true" })
185
+ }
186
+ )
187
+ ] }),
188
+ capturing && variant === "mic-button" && /* @__PURE__ */ jsxs(Fragment, { children: [
189
+ cancelButton,
190
+ /* @__PURE__ */ jsxs("span", { className: "flex flex-1 items-center gap-2 text-sm text-foreground", children: [
191
+ /* @__PURE__ */ jsx(
192
+ "span",
193
+ {
194
+ "aria-hidden": "true",
195
+ className: cn(
196
+ "size-2 rounded-full bg-destructive",
197
+ state === "recording" ? "animate-pulse motion-reduce:animate-none" : "opacity-40"
198
+ )
199
+ }
200
+ ),
201
+ state === "paused" ? "Paused" : listeningLabel,
202
+ duration && /* @__PURE__ */ jsxs("span", { className: "tabular-nums text-muted-foreground", children: [
203
+ "\xB7 ",
204
+ duration
205
+ ] })
206
+ ] }),
207
+ pauseButton,
208
+ /* @__PURE__ */ jsx(
209
+ "button",
210
+ {
211
+ type: "button",
212
+ "aria-label": "Stop and use recording",
213
+ onClick: onConfirm,
214
+ className: cn(
215
+ iconButton,
216
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
217
+ state === "recording" && "animate-pulse motion-reduce:animate-none"
218
+ ),
219
+ children: /* @__PURE__ */ jsx(Square, { className: "fill-current", "aria-hidden": "true" })
220
+ }
221
+ )
222
+ ] }),
223
+ state === "transcribing" && transcribing,
224
+ state === "error" && error,
225
+ liveRegion
226
+ ]
227
+ }
228
+ );
229
+ }
79
230
  );
80
231
  VoiceInput.displayName = "VoiceInput";
81
232
 
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.tsx"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import * as React from \"react\";\nimport { cn } from \"@nucleux/utils\";\nimport { Mic, X, Check, Loader2 } from \"lucide-react\";\n\nexport type VoiceInputState = \"idle\" | \"recording\" | \"transcribing\";\n\nexport interface VoiceInputProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Current state of the dictation flow. */\n state?: VoiceInputState;\n /** Elapsed time label while recording, e.g. `\"0:12\"`. */\n duration?: React.ReactNode;\n /** Transcribed text shown once available. */\n transcript?: React.ReactNode;\n /** Called from the idle mic button to start recording. */\n onStart?: () => void;\n /** Called to cancel/discard the recording. */\n onCancel?: () => void;\n /** Called to stop recording and confirm. */\n onConfirm?: () => void;\n}\n\nconst BAR_COUNT = 24;\n\nfunction Waveform() {\n return (\n <div className=\"flex h-8 flex-1 items-center gap-0.5\" aria-hidden=\"true\">\n {Array.from({ length: BAR_COUNT }).map((_, i) => (\n <span\n key={i}\n className=\"w-0.5 flex-1 rounded-full bg-info/70 animate-nx-typing\"\n style={{\n height: `${30 + ((i * 37) % 60)}%`,\n animationDelay: `${(i % 6) * 90}ms`,\n }}\n />\n ))}\n </div>\n );\n}\n\n/**\n * A composer in a live-dictation state — mic trigger, animated waveform while\n * recording, and a transcribing state — for voice input to the agent.\n * Source: Figma \"Expressive Input / Voice Input\".\n */\nexport const VoiceInput = React.forwardRef<HTMLDivElement, VoiceInputProps>(\n ({ state = \"idle\", duration, transcript, onStart, onCancel, onConfirm, className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\n \"flex items-center gap-3 rounded-full border border-border bg-background px-3 py-2\",\n className,\n )}\n role=\"group\"\n aria-label=\"Voice input\"\n {...props}\n >\n {state === \"idle\" && (\n <>\n <button\n type=\"button\"\n aria-label=\"Start dictation\"\n onClick={onStart}\n className=\"inline-flex size-9 items-center justify-center rounded-full bg-muted text-foreground transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&_svg]:size-4\"\n >\n <Mic aria-hidden=\"true\" />\n </button>\n <span className=\"flex-1 text-sm text-muted-foreground\">Tap to speak</span>\n </>\n )}\n\n {state === \"recording\" && (\n <>\n <button\n type=\"button\"\n aria-label=\"Cancel dictation\"\n onClick={onCancel}\n className=\"inline-flex size-9 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&_svg]:size-4\"\n >\n <X aria-hidden=\"true\" />\n </button>\n <Waveform />\n {duration && <span className=\"text-sm tabular-nums text-muted-foreground\">{duration}</span>}\n <button\n type=\"button\"\n aria-label=\"Stop and use recording\"\n onClick={onConfirm}\n className=\"inline-flex size-9 items-center justify-center rounded-full bg-info text-info-foreground transition-colors hover:bg-info/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&_svg]:size-4\"\n >\n <Check aria-hidden=\"true\" />\n </button>\n </>\n )}\n\n {state === \"transcribing\" && (\n <>\n <Loader2 className=\"size-4 shrink-0 animate-spin text-muted-foreground\" aria-hidden=\"true\" />\n <span className=\"flex-1 truncate text-sm text-muted-foreground\">\n {transcript ?? \"Transcribing…\"}\n </span>\n </>\n )}\n </div>\n ),\n);\nVoiceInput.displayName = \"VoiceInput\";\n"]}
1
+ {"version":3,"sources":["../src/index.tsx"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import * as React from \"react\";\nimport { cn } from \"@nucleux/utils\";\nimport { Button } from \"@nucleux/button\";\nimport { AlertCircle, Check, Loader2, Mic, Pause, Play, RotateCcw, Square, X } from \"lucide-react\";\n\n/**\n * `\"idle\"` → `\"recording\"` (⇄ `\"paused\"`) → `\"transcribing\"`; `\"error\"` when the speech\n * could not be captured or trusted.\n */\nexport type VoiceInputState = \"idle\" | \"recording\" | \"paused\" | \"transcribing\" | \"error\";\n\n/**\n * `\"waveform\"` (default) — the dictation bar with a live waveform;\n * `\"mic-button\"` — a compact tap-to-talk control with a clear stop state;\n * `\"transcript-stream\"` — a panel where the words appear as they are heard.\n */\nexport type VoiceInputVariant = \"waveform\" | \"mic-button\" | \"transcript-stream\";\n\nexport interface VoiceInputProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Presentation. Defaults to `\"waveform\"`. */\n variant?: VoiceInputVariant;\n /** Current state of the dictation flow. */\n state?: VoiceInputState;\n /** Elapsed time label while recording, e.g. `\"0:12\"`. */\n duration?: React.ReactNode;\n /** Transcribed text: the final words (shown while recording in `\"transcript-stream\"`, and once transcribing). */\n transcript?: React.ReactNode;\n /** Words still being heard (not final) — shown muted after `transcript`, with a caret. */\n interimTranscript?: React.ReactNode;\n /** Called from the idle mic button to start recording. */\n onStart?: () => void;\n /** Called to cancel/discard the recording. Typed text is untouched. */\n onCancel?: () => void;\n /** Called to stop recording and confirm. The transcript goes to the field for review — it never sends. */\n onConfirm?: () => void;\n /** Called to pause capture (mic off, progress kept). Renders a Pause control while recording. */\n onPause?: () => void;\n /** Called to resume a paused recording. */\n onResume?: () => void;\n /** Called from the error state to try again. */\n onRetry?: () => void;\n /** Label shown while idle. Defaults to `\"Tap to speak\"`. */\n idleLabel?: React.ReactNode;\n /** Label shown while capturing, e.g. `\"Aria is listening\"`. Defaults to `\"Listening\"`. */\n listeningLabel?: React.ReactNode;\n /** Message shown in the error state. Defaults to `\"Couldn’t make out all of that.\"`. */\n errorText?: React.ReactNode;\n}\n\nconst BAR_COUNT = 24;\n\nfunction Waveform({ paused = false }: { paused?: boolean }) {\n return (\n <div className=\"flex h-8 flex-1 items-center gap-0.5\" aria-hidden=\"true\">\n {Array.from({ length: BAR_COUNT }).map((_, i) => (\n <span\n key={i}\n className={cn(\n \"w-0.5 flex-1 rounded-full bg-info/70 motion-reduce:animate-none\",\n paused ? \"opacity-40\" : \"animate-nx-typing\",\n )}\n style={{\n height: `${30 + ((i * 37) % 60)}%`,\n animationDelay: `${(i % 6) * 90}ms`,\n }}\n />\n ))}\n </div>\n );\n}\n\nconst iconButton =\n \"inline-flex size-9 items-center justify-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&_svg]:size-4\";\n\nconst textOf = (node: React.ReactNode, fallback: string) =>\n typeof node === \"string\" || typeof node === \"number\" ? String(node) : fallback;\n\n/**\n * Voice input: speech as an input method into the composer — a mic trigger,\n * a clearly lit capturing state (waveform or pulsing stop button, clock, live\n * words), Pause/Resume, Cancel and Stop, then a transcribing state. Stop never\n * sends: the transcript goes to the field for the person to review. State is\n * announced through a polite live region and every animation stops under\n * reduced motion. Variants: waveform, mic-button, transcript-stream.\n * Source: Figma \"Expressive Input / Voice Input\".\n */\nexport const VoiceInput = React.forwardRef<HTMLDivElement, VoiceInputProps>(\n (\n {\n variant = \"waveform\",\n state = \"idle\",\n duration,\n transcript,\n interimTranscript,\n onStart,\n onCancel,\n onConfirm,\n onPause,\n onResume,\n onRetry,\n idleLabel = \"Tap to speak\",\n listeningLabel = \"Listening\",\n errorText = \"Couldn’t make out all of that.\",\n className,\n ...props\n },\n ref,\n ) => {\n const capturing = state === \"recording\" || state === \"paused\";\n const announcement =\n state === \"recording\"\n ? textOf(listeningLabel, \"Listening\")\n : state === \"paused\"\n ? \"Paused\"\n : state === \"transcribing\"\n ? \"Transcribing\"\n : state === \"error\"\n ? textOf(errorText, \"Voice input failed\")\n : \"\";\n\n const liveRegion = (\n <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {announcement}\n </span>\n );\n\n const startButton = (\n <button\n type=\"button\"\n aria-label=\"Start dictation\"\n onClick={onStart}\n className={cn(iconButton, \"bg-muted text-foreground hover:bg-accent\")}\n >\n <Mic aria-hidden=\"true\" />\n </button>\n );\n\n const cancelButton = (\n <button\n type=\"button\"\n aria-label=\"Cancel dictation\"\n onClick={onCancel}\n className={cn(iconButton, \"text-muted-foreground hover:bg-accent hover:text-foreground\")}\n >\n <X aria-hidden=\"true\" />\n </button>\n );\n\n const pauseButton =\n state === \"paused\"\n ? onResume && (\n <Button variant=\"ghost\" size=\"icon\" aria-label=\"Resume dictation\" onClick={onResume} className=\"rounded-full\">\n <Play className=\"size-4\" aria-hidden=\"true\" />\n </Button>\n )\n : onPause && (\n <Button variant=\"ghost\" size=\"icon\" aria-label=\"Pause dictation\" onClick={onPause} className=\"rounded-full\">\n <Pause className=\"size-4\" aria-hidden=\"true\" />\n </Button>\n );\n\n const transcribing = (\n <>\n <Loader2\n className=\"size-4 shrink-0 animate-spin text-muted-foreground motion-reduce:animate-none\"\n aria-hidden=\"true\"\n />\n <span className=\"flex-1 truncate text-sm text-muted-foreground\">{transcript ?? \"Transcribing…\"}</span>\n </>\n );\n\n const error = (\n <>\n <AlertCircle className=\"size-4 shrink-0 text-destructive\" aria-hidden=\"true\" />\n <span className=\"flex-1 truncate text-sm text-destructive\">{errorText}</span>\n {onRetry && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n leftIcon={<RotateCcw className=\"size-4\" aria-hidden=\"true\" />}\n onClick={onRetry}\n >\n Retry\n </Button>\n )}\n {onCancel && cancelButton}\n </>\n );\n\n if (variant === \"transcript-stream\") {\n return (\n <div\n ref={ref}\n className={cn(\"flex flex-col gap-2 rounded-xl border border-border bg-background px-4 py-3\", className)}\n role=\"group\"\n aria-label=\"Voice input\"\n data-state={state}\n data-variant={variant}\n {...props}\n >\n {state === \"idle\" ? (\n <div className=\"flex items-center gap-3\">\n {startButton}\n <span className=\"flex-1 text-sm text-muted-foreground\">{idleLabel}</span>\n </div>\n ) : state === \"error\" ? (\n <div className=\"flex items-center gap-2\">{error}</div>\n ) : (\n <>\n <p className=\"min-h-5 text-sm leading-5 text-foreground\">\n {transcript}\n {interimTranscript && (\n <>\n {transcript ? \" \" : null}\n <span className=\"italic text-muted-foreground\">{interimTranscript}</span>\n </>\n )}\n {capturing && (\n <span\n aria-hidden=\"true\"\n className=\"ml-px inline-block h-3.5 w-0.5 translate-y-0.5 bg-brand animate-nx-caret-blink motion-reduce:animate-none\"\n />\n )}\n </p>\n <div className=\"flex items-center gap-2\">\n {state === \"transcribing\" ? (\n <>\n <Loader2\n className=\"size-3.5 shrink-0 animate-spin text-muted-foreground motion-reduce:animate-none\"\n aria-hidden=\"true\"\n />\n <span className=\"flex-1 text-xs text-muted-foreground\">Transcribing…</span>\n </>\n ) : (\n <>\n <Mic className={cn(\"size-3.5 shrink-0\", state === \"paused\" ? \"text-muted-foreground\" : \"text-destructive\")} aria-hidden=\"true\" />\n <span className=\"flex-1 text-xs text-muted-foreground\">\n {state === \"paused\" ? \"Paused\" : listeningLabel}\n {duration && <span className=\"tabular-nums\"> · {duration}</span>}\n </span>\n {pauseButton}\n {onCancel && cancelButton}\n <Button variant=\"cta\" size=\"sm\" onClick={onConfirm} leftIcon={<Check className=\"size-4\" aria-hidden=\"true\" />}>\n Stop\n </Button>\n </>\n )}\n </div>\n </>\n )}\n {liveRegion}\n </div>\n );\n }\n\n return (\n <div\n ref={ref}\n className={cn(\n \"flex items-center gap-3 rounded-full border border-border bg-background px-3 py-2\",\n className,\n )}\n role=\"group\"\n aria-label=\"Voice input\"\n data-state={state}\n data-variant={variant}\n {...props}\n >\n {state === \"idle\" && (\n <>\n {startButton}\n <span className=\"flex-1 text-sm text-muted-foreground\">{idleLabel}</span>\n </>\n )}\n\n {capturing && variant === \"waveform\" && (\n <>\n {cancelButton}\n <Waveform paused={state === \"paused\"} />\n {duration && <span className=\"text-sm tabular-nums text-muted-foreground\">{duration}</span>}\n {pauseButton}\n <button\n type=\"button\"\n aria-label=\"Stop and use recording\"\n onClick={onConfirm}\n className={cn(iconButton, \"bg-info text-info-foreground hover:bg-info/90\")}\n >\n <Check aria-hidden=\"true\" />\n </button>\n </>\n )}\n\n {capturing && variant === \"mic-button\" && (\n <>\n {cancelButton}\n <span className=\"flex flex-1 items-center gap-2 text-sm text-foreground\">\n <span\n aria-hidden=\"true\"\n className={cn(\n \"size-2 rounded-full bg-destructive\",\n state === \"recording\" ? \"animate-pulse motion-reduce:animate-none\" : \"opacity-40\",\n )}\n />\n {state === \"paused\" ? \"Paused\" : listeningLabel}\n {duration && <span className=\"tabular-nums text-muted-foreground\">· {duration}</span>}\n </span>\n {pauseButton}\n <button\n type=\"button\"\n aria-label=\"Stop and use recording\"\n onClick={onConfirm}\n className={cn(\n iconButton,\n \"bg-destructive text-destructive-foreground hover:bg-destructive/90\",\n state === \"recording\" && \"animate-pulse motion-reduce:animate-none\",\n )}\n >\n <Square className=\"fill-current\" aria-hidden=\"true\" />\n </button>\n </>\n )}\n\n {state === \"transcribing\" && transcribing}\n {state === \"error\" && error}\n {liveRegion}\n </div>\n );\n },\n);\nVoiceInput.displayName = \"VoiceInput\";\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nucleux/voice-input",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "VoiceInput — a live dictation bar with a waveform — Nucleux agentic UI for React.",
@@ -33,7 +33,8 @@
33
33
  "react-dom": ">=18"
34
34
  },
35
35
  "dependencies": {
36
- "@nucleux/utils": "0.5.0",
36
+ "@nucleux/button": "0.6.0",
37
+ "@nucleux/utils": "0.6.0",
37
38
  "lucide-react": "^0.469.0"
38
39
  },
39
40
  "devDependencies": {