@designcrowd/fe-shared-lib 1.8.5-edge-fallback-5 → 1.8.5

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/index.js CHANGED
@@ -23,7 +23,6 @@ export { WEBSITE_UPGRADE_CONTEXT_TYPES } from './src/experiences/models/websiteC
23
23
 
24
24
  export { setSharedLibLocaleAsync, tr } from './src/useSharedLibTranslate';
25
25
  export { useVoiceToText } from './src/useVoiceToText';
26
- export { setVoiceToTextSessionDisabledForTesting, VOICE_UNAVAILABLE_MESSAGE } from './src/useVoiceToText';
27
26
 
28
27
  export { default as Button } from './src/atoms/components/Button/Button.vue';
29
28
  export { default as ButtonGroup } from './src/atoms/components/ButtonGroup/ButtonGroup.vue';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@designcrowd/fe-shared-lib",
3
- "version": "1.8.5-edge-fallback-5",
3
+ "version": "1.8.5",
4
4
  "scripts": {
5
5
  "start": "run-p storybook watch:translation",
6
6
  "build": "npm run build:css --production",
@@ -1,17 +1,19 @@
1
1
  #!/usr/bin/env bash
2
2
  #
3
- # publish-uat.sh — publish a UAT-suffixed version of @designcrowd/fe-shared-lib
3
+ # publish-uat.sh — publish @designcrowd/fe-shared-lib (UAT or stable)
4
4
  #
5
- # Usage: ./scripts/publish-uat.sh <suffix>
5
+ # Usage: ./scripts/publish-uat.sh <suffix> # UAT pre-release
6
+ # ./scripts/publish-uat.sh --stable # final release (no suffix)
6
7
  # Example: ./scripts/publish-uat.sh edge-fallback-2
8
+ # ./scripts/publish-uat.sh --stable
7
9
  #
8
10
  # Reads the npm publish token from $NPM_PUBLISH_TOKEN_FILE
9
11
  # (default ~/.config/designcrowd/npm-publish-token, mode 600).
10
12
  # The token is never written into the repo.
11
13
  #
12
14
  # Workflow:
13
- # 1. Validate branch (refuse master) and suffix.
14
- # 2. Compute new version = <base>-<suffix>, refuse if already published.
15
+ # 1. Validate args; in UAT mode also refuse master/main.
16
+ # 2. Compute new version (<base>-<suffix> or <base>); refuse if already published.
15
17
  # 3. Bump package.json, run bundle-translation.
16
18
  # 4. npm pack — produces a tarball.
17
19
  # 5. Secret scan: dangerous filenames + token-shaped strings + the
@@ -33,16 +35,28 @@ TOKEN_FILE="${NPM_PUBLISH_TOKEN_FILE:-$HOME/.config/designcrowd/npm-publish-toke
33
35
  abort() { echo "ABORT: $*" >&2; exit 1; }
34
36
 
35
37
  # --- args ---
36
- SUFFIX="${1:-}"
37
- [[ -z "$SUFFIX" ]] && abort "usage: $0 <suffix> e.g. $0 edge-fallback-2"
38
- # Semver pre-release identifiers are [0-9A-Za-z-], dot-separated. Reject underscores etc.
39
- # so we fail before npm rejects the version mid-flight with a dirty tree.
40
- [[ "$SUFFIX" =~ ^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*$ ]] || abort "suffix must match semver pre-release: alphanumerics/hyphens, optionally dot-separated"
38
+ MODE="uat"
39
+ SUFFIX=""
40
+ ARG="${1:-}"
41
+ if [[ "$ARG" == "--stable" ]]; then
42
+ MODE="stable"
43
+ elif [[ -n "$ARG" ]]; then
44
+ SUFFIX="$ARG"
45
+ # Semver pre-release identifiers are [0-9A-Za-z-], dot-separated. Reject underscores etc.
46
+ # so we fail before npm rejects the version mid-flight with a dirty tree.
47
+ [[ "$SUFFIX" =~ ^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*$ ]] || abort "suffix must match semver pre-release: alphanumerics/hyphens, optionally dot-separated"
48
+ else
49
+ abort $'usage: '"$0"$' <suffix> # UAT pre-release (e.g. edge-fallback-2)\n '"$0"$' --stable # final release'
50
+ fi
41
51
 
42
52
  # --- repo state ---
43
53
  cd "$(git rev-parse --show-toplevel)"
44
54
  BRANCH=$(git symbolic-ref --short HEAD)
45
- [[ "$BRANCH" == "master" || "$BRANCH" == "main" ]] && abort "refusing to publish from $BRANCH"
55
+ # UAT publishes from feature branches only; stable can come from any branch
56
+ # (master is the typical case but feature-branch hot publishes are allowed too).
57
+ if [[ "$MODE" == "uat" ]]; then
58
+ [[ "$BRANCH" == "master" || "$BRANCH" == "main" ]] && abort "refusing UAT publish from $BRANCH"
59
+ fi
46
60
 
47
61
  # --- preconditions: files we'll commit must be clean, so unrelated edits don't get swept into the version-bump commit ---
48
62
  [[ -z "$(git status --porcelain -- package.json)" ]] || abort "package.json has uncommitted changes; commit or stash before publishing"
@@ -70,10 +84,18 @@ fi
70
84
 
71
85
  # --- compute version ---
72
86
  BASE=$(node -p "require('./package.json').version.replace(/-.*/, '')")
73
- NEW="$BASE-$SUFFIX"
87
+ if [[ "$MODE" == "stable" ]]; then
88
+ NEW="$BASE"
89
+ else
90
+ NEW="$BASE-$SUFFIX"
91
+ fi
74
92
 
75
93
  if npm view "$PKG@$NEW" version >/dev/null 2>&1; then
76
- abort "$PKG@$NEW already published pick a new suffix"
94
+ if [[ "$MODE" == "stable" ]]; then
95
+ abort "$PKG@$NEW already published — bump the base version in package.json first"
96
+ else
97
+ abort "$PKG@$NEW already published — pick a new suffix"
98
+ fi
77
99
  fi
78
100
 
79
101
  echo "==> publishing $PKG@$NEW from branch $BRANCH"
@@ -1,5 +1,4 @@
1
1
  import VoiceToTextButton from './VoiceToTextButton.vue';
2
- import { setVoiceToTextSessionDisabledForTesting } from '../../../useVoiceToText';
3
2
 
4
3
  export default {
5
4
  title: 'Components/VoiceToTextButton',
@@ -241,124 +240,3 @@ export const SideBySide = () => ({
241
240
  SideBySide.story = {
242
241
  name: 'Side by Side Comparison',
243
242
  };
244
-
245
- export const ForceUnsupported = () => ({
246
- components: { VoiceToTextButton },
247
- methods: {
248
- forceUnsupported() {
249
- setVoiceToTextSessionDisabledForTesting(true);
250
- },
251
- reset() {
252
- setVoiceToTextSessionDisabledForTesting(false);
253
- },
254
- },
255
- template: `
256
- <div class="tw-min-h-[400px] tw-p-8 tw-flex tw-flex-col tw-items-center tw-justify-center tw-bg-white">
257
- <h3 class="tw-text-grayscale-800 tw-text-lg tw-font-semibold tw-mb-2">Force unsupported preview</h3>
258
- <p class="tw-text-grayscale-600 tw-text-sm tw-mb-6 tw-text-center tw-max-w-md">
259
- Simulates the Edge fallback: after a 'network' SpeechRecognitionError, the composable latches
260
- <code class="tw-bg-grayscale-100 tw-px-1 tw-rounded">isSupported</code> to false for the rest of the session.
261
- The button now stays visible but disabled, with a tooltip explaining the situation. Reset clears the session flag.
262
- </p>
263
-
264
- <div class="tw-flex tw-gap-3 tw-mb-6">
265
- <button
266
- type="button"
267
- @click="forceUnsupported"
268
- class="tw-px-4 tw-py-2 tw-rounded tw-bg-error-500 tw-text-white tw-text-sm hover:tw-bg-error-600"
269
- >
270
- Force unsupported
271
- </button>
272
- <button
273
- type="button"
274
- @click="reset"
275
- class="tw-px-4 tw-py-2 tw-rounded tw-bg-grayscale-200 tw-text-grayscale-800 tw-text-sm hover:tw-bg-grayscale-300"
276
- >
277
- Reset
278
- </button>
279
- </div>
280
-
281
- <div class="tw-w-full tw-max-w-xl tw-bg-grayscale-100 tw-rounded-full tw-px-6 tw-py-3 tw-flex tw-items-center tw-gap-3 tw-border tw-border-grayscale-300 tw-min-h-[64px]">
282
- <input
283
- type="text"
284
- placeholder="Voice input would appear on the right..."
285
- class="tw-flex-1 tw-bg-transparent tw-border-none tw-text-grayscale-800 tw-placeholder-grayscale-500 focus:tw-outline-none tw-text-base"
286
- />
287
- <VoiceToTextButton variant="light" size="md" />
288
- </div>
289
-
290
- <p class="tw-text-grayscale-500 tw-text-xs tw-mt-4">
291
- Tip: open DevTools → Application → Session Storage to see the
292
- <code class="tw-bg-grayscale-100 tw-px-1 tw-rounded">fe-shared-lib:voice-to-text-disabled</code> key.
293
- </p>
294
- </div>
295
- `,
296
- });
297
-
298
- ForceUnsupported.story = {
299
- name: 'Force Unsupported (Edge Fallback)',
300
- };
301
-
302
- export const ControlVsUnsupported = () => ({
303
- components: { VoiceToTextButton },
304
- methods: {
305
- forceUnsupported() {
306
- setVoiceToTextSessionDisabledForTesting(true);
307
- },
308
- reset() {
309
- setVoiceToTextSessionDisabledForTesting(false);
310
- },
311
- },
312
- template: `
313
- <div class="tw-min-h-[400px] tw-p-8" style="background: #1a1a2e;">
314
- <h3 class="tw-text-white tw-text-lg tw-font-semibold tw-mb-2">Control vs Unsupported (side-by-side)</h3>
315
- <p class="tw-text-grayscale-400 tw-text-sm tw-mb-6 tw-max-w-2xl">
316
- Both buttons share variant + size. The right one is rendered with the session-disabled latch
317
- flipped on, so it shows the new disabled state with the explanatory tooltip. Use this to compare
318
- visual treatments for design review.
319
- </p>
320
-
321
- <div class="tw-flex tw-gap-3 tw-mb-6">
322
- <button
323
- type="button"
324
- @click="forceUnsupported"
325
- class="tw-px-4 tw-py-2 tw-rounded tw-bg-error-500 tw-text-white tw-text-sm hover:tw-bg-error-600"
326
- >
327
- Force unsupported
328
- </button>
329
- <button
330
- type="button"
331
- @click="reset"
332
- class="tw-px-4 tw-py-2 tw-rounded tw-bg-grayscale-200 tw-text-grayscale-800 tw-text-sm hover:tw-bg-grayscale-300"
333
- >
334
- Reset
335
- </button>
336
- </div>
337
-
338
- <div class="tw-flex tw-gap-12 tw-items-start">
339
- <div class="tw-text-center">
340
- <div class="tw-mb-2 tw-p-4 tw-bg-grayscale-800 tw-rounded-lg tw-inline-block">
341
- <VoiceToTextButton variant="dark" size="md" />
342
- </div>
343
- <p class="tw-text-grayscale-400 tw-text-xs">Control (default state)</p>
344
- </div>
345
-
346
- <div class="tw-text-center">
347
- <div class="tw-mb-2 tw-p-4 tw-bg-grayscale-800 tw-rounded-lg tw-inline-block">
348
- <VoiceToTextButton variant="dark" size="md" />
349
- </div>
350
- <p class="tw-text-grayscale-400 tw-text-xs">Unsupported (after latch)</p>
351
- </div>
352
- </div>
353
-
354
- <p class="tw-text-grayscale-500 tw-text-xs tw-mt-6">
355
- Note: the composable is a singleton, so flipping the latch affects every mounted instance —
356
- which is exactly what the production Edge case does. Both buttons will switch together.
357
- </p>
358
- </div>
359
- `,
360
- });
361
-
362
- ControlVsUnsupported.story = {
363
- name: 'Control vs Unsupported',
364
- };
@@ -1,23 +1,17 @@
1
1
  <template>
2
2
  <button
3
+ v-if="isSupported"
3
4
  type="button"
4
- :disabled="disabled || unavailableForSession"
5
- :aria-label="
6
- unavailableForSession
7
- ? 'Voice input not available in this browser'
8
- : isListening
9
- ? 'Stop voice input'
10
- : 'Start voice input'
11
- "
12
- :title="effectiveTitle"
5
+ :disabled="disabled"
6
+ :aria-label="isListening ? 'Stop voice input' : 'Start voice input'"
7
+ :title="title"
13
8
  data-test="voice-to-text-button"
14
9
  class="voice-prompt-button tw-rounded-full tw-border-0 tw-flex tw-items-center tw-justify-center tw-transition-all tw-duration-200 tw-cursor-pointer focus:tw-outline-none focus-visible:tw-ring-2 focus-visible:tw-ring-offset-2 focus-visible:tw-ring-primary-500"
15
10
  :class="[
16
11
  sizeClasses,
17
12
  variantClasses.button,
18
13
  isListening ? 'voice-prompt-listening' : '',
19
- (disabled || unavailableForSession) ? 'tw-opacity-50 tw-cursor-not-allowed' : '',
20
- unavailableForSession ? 'voice-prompt-unavailable' : '',
14
+ disabled ? 'tw-opacity-50 tw-cursor-not-allowed' : '',
21
15
  ]"
22
16
  :style="isListening ? { '--voice-bg': variantClasses.recordingBg } : {}"
23
17
  @click="toggle"
@@ -34,7 +28,7 @@
34
28
  <script setup lang="ts">
35
29
  import { watch, toRef, computed } from 'vue';
36
30
  import Icon from '../Icon/Icon.vue';
37
- import { useVoiceToText, VOICE_UNAVAILABLE_MESSAGE } from '../../../useVoiceToText';
31
+ import { useVoiceToText } from '../../../useVoiceToText';
38
32
 
39
33
  type ButtonSize = 'sm' | 'md' | 'lg';
40
34
  type ButtonVariant = 'light' | 'dark';
@@ -100,13 +94,8 @@ const variantClasses = computed(() => {
100
94
  };
101
95
  });
102
96
 
103
- const unavailableForSession = computed(() => !isSupported.value);
104
- const effectiveTitle = computed(() => (unavailableForSession.value ? VOICE_UNAVAILABLE_MESSAGE : props.title));
105
-
106
- // Keep recognition language in sync with prop
107
97
  watch(toRef(props, 'lang'), setLang);
108
98
 
109
- // Watch for transcript changes and emit appropriate events
110
99
  watch(
111
100
  [transcript, isFinal],
112
101
  ([newTranscript, newIsFinal]) => {
@@ -119,7 +108,6 @@ watch(
119
108
  { flush: 'sync' },
120
109
  );
121
110
 
122
- // Watch for listening state changes
123
111
  watch(isListening, (newVal, oldVal) => {
124
112
  if (newVal && !oldVal) {
125
113
  emit('on-start');
@@ -129,7 +117,6 @@ watch(isListening, (newVal, oldVal) => {
129
117
  }
130
118
  });
131
119
 
132
- // Watch for errors
133
120
  watch(error, (newError) => {
134
121
  if (newError) {
135
122
  emit('on-error', newError);
@@ -156,23 +143,4 @@ watch(error, (newError) => {
156
143
  opacity: 0.5;
157
144
  }
158
145
  }
159
-
160
- .voice-prompt-unavailable {
161
- position: relative;
162
- }
163
-
164
- .voice-prompt-unavailable::after {
165
- content: '';
166
- position: absolute;
167
- inset: 18%;
168
- background: linear-gradient(
169
- 135deg,
170
- transparent calc(50% - 1px),
171
- #E34935 calc(50% - 1px),
172
- #E34935 calc(50% + 1px),
173
- transparent calc(50% + 1px)
174
- );
175
- pointer-events: none;
176
- border-radius: inherit;
177
- }
178
146
  </style>
@@ -25,22 +25,11 @@ interface VoiceToTextState {
25
25
  error: Ref<string | null>;
26
26
  }
27
27
 
28
- // Singleton instance and state (lazily initialized)
29
28
  let recognition: SpeechRecognition | null = null;
30
29
  let isInitialized = false;
31
30
  let errorClearTimeout: ReturnType<typeof setTimeout> | null = null;
32
31
  let state: VoiceToTextState | null = null;
33
- let sessionDisabled: Ref<boolean> | null = null;
34
- let hasReceivedResult = false;
35
32
 
36
- // Edge ships window.SpeechRecognition but routes it through Microsoft's Online
37
- // Speech service, which is gated by a Windows privacy toggle that's commonly
38
- // off — start() then fires a 'network' error. There's no synchronous capability
39
- // check, so we latch on the first network error and treat the feature as
40
- // unsupported for the rest of the session. See issue #161.
41
- const SESSION_DISABLED_KEY = 'fe-shared-lib:voice-to-text-disabled';
42
-
43
- // Error message mapping per spec
44
33
  const ERROR_MESSAGES: Record<string, string> = {
45
34
  'not-allowed': 'Microphone permission was denied. Please allow access.',
46
35
  'language-not-supported': 'This language is not supported.',
@@ -48,22 +37,15 @@ const ERROR_MESSAGES: Record<string, string> = {
48
37
  'audio-capture': 'No microphone was found or microphone is not working.',
49
38
  };
50
39
 
51
- export const VOICE_UNAVAILABLE_MESSAGE =
52
- "Voice input isn't available in this browser. Try Chrome or Safari for the best experience.";
53
-
54
40
  const ERROR_CLEAR_DELAY = 5000;
55
41
 
56
- async function checkMicPermission(): Promise<'granted' | 'denied' | 'prompt' | 'unknown'> {
57
- try {
58
- if (typeof navigator === 'undefined' || !navigator.permissions?.query) {
59
- return 'unknown';
60
- }
61
- const result = await navigator.permissions.query({ name: 'microphone' as PermissionName });
62
- return result.state as 'granted' | 'denied' | 'prompt';
63
- } catch {
64
- // Permissions API or the 'microphone' descriptor isn't supported (older Safari, Firefox quirks).
65
- return 'unknown';
66
- }
42
+ // Edge exposes window.SpeechRecognition but routes it through Microsoft's
43
+ // Online Speech service, which fails inconsistently in ways the spec doesn't
44
+ // cover (network errors, not-allowed even when mic is granted, mic UI
45
+ // disappearing after first use). Treat Edge as unsupported until MS fixes it.
46
+ function isEdgeBrowser(): boolean {
47
+ if (typeof navigator === 'undefined' || !navigator.userAgent) return false;
48
+ return /Edg(e|A|iOS)?\//.test(navigator.userAgent);
67
49
  }
68
50
 
69
51
  function getState(): VoiceToTextState {
@@ -78,54 +60,6 @@ function getState(): VoiceToTextState {
78
60
  return state;
79
61
  }
80
62
 
81
- function getSessionDisabled(): Ref<boolean> {
82
- if (!sessionDisabled) {
83
- let initial = false;
84
- try {
85
- initial = typeof window !== 'undefined' && window.sessionStorage?.getItem(SESSION_DISABLED_KEY) === '1';
86
- } catch {
87
- // sessionStorage can throw in sandboxed iframes / disabled-cookie contexts
88
- }
89
- sessionDisabled = ref(initial);
90
- }
91
- return sessionDisabled;
92
- }
93
-
94
- function setSessionDisabled(disabled: boolean): void {
95
- const flag = getSessionDisabled();
96
- flag.value = disabled;
97
- try {
98
- if (typeof window === 'undefined') return;
99
- if (disabled) {
100
- window.sessionStorage?.setItem(SESSION_DISABLED_KEY, '1');
101
- } else {
102
- window.sessionStorage?.removeItem(SESSION_DISABLED_KEY);
103
- }
104
- } catch {
105
- // ignore storage failures — in-memory flag still applies for this session
106
- }
107
- }
108
-
109
- function latchAsUnavailable(): void {
110
- const { error: errorRef } = getState();
111
- setSessionDisabled(true);
112
- errorRef.value = VOICE_UNAVAILABLE_MESSAGE;
113
- if (errorClearTimeout) {
114
- clearTimeout(errorClearTimeout);
115
- }
116
- errorClearTimeout = setTimeout(() => {
117
- errorRef.value = null;
118
- }, ERROR_CLEAR_DELAY);
119
- }
120
-
121
- /**
122
- * Test/Storybook helper: force the session-disabled latch on or off without
123
- * needing a real network error. Not part of the public consumer API.
124
- */
125
- export function setVoiceToTextSessionDisabledForTesting(disabled: boolean): void {
126
- setSessionDisabled(disabled);
127
- }
128
-
129
63
  /**
130
64
  * Singleton composable that wraps the Web Speech API (SpeechRecognition).
131
65
  * All calls to useVoiceToText() return the same shared instance.
@@ -133,18 +67,15 @@ export function setVoiceToTextSessionDisabledForTesting(disabled: boolean): void
133
67
  export function useVoiceToText(options: UseVoiceToTextOptions = {}): UseVoiceToTextReturn {
134
68
  const { lang = 'en-US' } = options;
135
69
 
136
- // Get or create shared state
137
70
  const { isListening, transcript, isFinal, error } = getState();
138
71
 
139
- // Check for browser support
140
72
  const SpeechRecognitionCtor: typeof SpeechRecognition | null =
141
73
  typeof window !== 'undefined' ? window.SpeechRecognition || window.webkitSpeechRecognition : null;
142
74
 
143
- const sessionDisabledRef = getSessionDisabled();
144
- const isSupported = computed(() => !!SpeechRecognitionCtor && !sessionDisabledRef.value);
75
+ const supported = !!SpeechRecognitionCtor && !isEdgeBrowser();
76
+ const isSupported = computed(() => supported);
145
77
 
146
- // Initialize singleton once
147
- if (!isInitialized && SpeechRecognitionCtor) {
78
+ if (!isInitialized && supported && SpeechRecognitionCtor) {
148
79
  recognition = new SpeechRecognitionCtor();
149
80
  recognition.continuous = true;
150
81
  recognition.interimResults = true;
@@ -162,74 +93,22 @@ export function useVoiceToText(options: UseVoiceToTextOptions = {}): UseVoiceToT
162
93
  }
163
94
  }
164
95
 
165
- if (finalTranscript.length > 0 || interimTranscript.length > 0) {
166
- hasReceivedResult = true;
167
- }
168
-
169
96
  transcript.value = finalTranscript + interimTranscript;
170
97
  isFinal.value = interimTranscript === '';
171
98
  };
172
99
 
173
- recognition.onerror = async (event: SpeechRecognitionErrorEvent) => {
100
+ recognition.onerror = (event: SpeechRecognitionErrorEvent) => {
174
101
  // Suppress no-speech and aborted errors per spec
175
102
  if (event.error === 'no-speech' || event.error === 'aborted') {
176
103
  return;
177
104
  }
178
105
 
179
- // Diagnostic instrumentation: capture state at every non-suppressed error
180
- // so QA has a data point on first failure across browser/permission combos.
181
- const micPermission = await checkMicPermission();
182
- const sessionDisabledFlag = getSessionDisabled().value;
183
- // eslint-disable-next-line no-console
184
- console.warn('[useVoiceToText] error event', {
185
- error: event.error,
186
- micPermission,
187
- hasReceivedResult,
188
- sessionDisabled: sessionDisabledFlag,
189
- });
190
-
191
- if (event.error === 'network') {
192
- if (!hasReceivedResult) {
193
- // First-attempt network failure on Edge with the privacy toggle off (or
194
- // an actually-broken network). Latch the feature off for the session.
195
- // eslint-disable-next-line no-console
196
- console.warn('[useVoiceToText] network error before any result — disabling voice input for this session');
197
- isListening.value = false;
198
- latchAsUnavailable();
199
- } else {
200
- // Trailing network error after a successful result is a known Edge teardown
201
- // quirk — swallow it instead of latching, since voice clearly works for this user.
202
- // eslint-disable-next-line no-console
203
- console.warn('[useVoiceToText] swallowing trailing network error after successful result');
204
- isListening.value = false;
205
- }
206
- return;
207
- }
208
-
209
- if (event.error === 'not-allowed') {
210
- if (micPermission === 'granted') {
211
- // Page-level mic permission was granted but the underlying speech backend
212
- // (Edge's Microsoft Online Speech service) refused. This is the Mac
213
- // InPrivate / Windows-toggle-off failure mode. Latch instead of showing a
214
- // misleading "permission denied" toast.
215
- // eslint-disable-next-line no-console
216
- console.warn(
217
- '[useVoiceToText] not-allowed despite granted mic permission — disabling voice input for this session',
218
- );
219
- isListening.value = false;
220
- latchAsUnavailable();
221
- return;
222
- }
223
- // Otherwise fall through to the existing "permission was denied" toast.
224
- }
225
-
226
106
  const message = ERROR_MESSAGES[event.error] || 'An error occurred with speech recognition.';
227
107
  error.value = message;
228
108
 
229
109
  // eslint-disable-next-line no-console
230
110
  console.warn('[useVoiceToText]', event.error, message);
231
111
 
232
- // Auto-clear error after timeout
233
112
  if (errorClearTimeout) {
234
113
  clearTimeout(errorClearTimeout);
235
114
  }
@@ -245,7 +124,6 @@ export function useVoiceToText(options: UseVoiceToTextOptions = {}): UseVoiceToT
245
124
  isInitialized = true;
246
125
  }
247
126
 
248
- // Update language on existing instance
249
127
  if (recognition) {
250
128
  recognition.lang = lang;
251
129
  }
@@ -253,7 +131,6 @@ export function useVoiceToText(options: UseVoiceToTextOptions = {}): UseVoiceToT
253
131
  const start = () => {
254
132
  if (!recognition || isListening.value) return;
255
133
 
256
- // Clear previous state
257
134
  transcript.value = '';
258
135
  isFinal.value = false;
259
136
  error.value = null;
@@ -266,7 +143,6 @@ export function useVoiceToText(options: UseVoiceToTextOptions = {}): UseVoiceToT
266
143
  recognition.start();
267
144
  isListening.value = true;
268
145
  } catch (e: unknown) {
269
- // Handle case where recognition is already started
270
146
  // eslint-disable-next-line no-console
271
147
  console.warn('[useVoiceToText] Failed to start:', (e as Error).message);
272
148
  }
@@ -279,7 +155,6 @@ export function useVoiceToText(options: UseVoiceToTextOptions = {}): UseVoiceToT
279
155
  try {
280
156
  recognition.stop();
281
157
  } catch (e: unknown) {
282
- // Handle case where recognition is already stopped
283
158
  // eslint-disable-next-line no-console
284
159
  console.warn('[useVoiceToText] Failed to stop:', (e as Error).message);
285
160
  }