@usero/sdk 1.1.6 → 1.1.7

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.
@@ -219,12 +219,37 @@ function buildIndicator(host, store, callbacks) {
219
219
  color: #fcd34d;
220
220
  }
221
221
  .mic[data-mic-state="muted"]:hover { background: rgba(251, 191, 36, 0.26); }
222
- .mic[data-mic-state="none"] {
223
- background: rgba(255,255,255,0.04);
224
- color: rgba(255,255,255,0.55);
222
+ /* Connecting: getUserMedia pending. Steady amber tint reads as "working",
223
+ distinct from the live red pulse and the failed state below. The icon
224
+ gets a gentle non-pulsing breathe so it feels alive without alarming. */
225
+ .mic[data-mic-state="connecting"] {
226
+ background: rgba(251, 191, 36, 0.14);
227
+ border-color: rgba(251, 191, 36, 0.32);
228
+ color: #fcd34d;
225
229
  cursor: default;
226
230
  }
227
- .mic[data-mic-state="none"]:hover { background: rgba(255,255,255,0.04); }
231
+ .mic[data-mic-state="connecting"]:hover { background: rgba(251, 191, 36, 0.14); }
232
+ .mic[data-mic-state="connecting"] .mic-icon {
233
+ color: #fbbf24;
234
+ animation: micBreathe 1.4s ease-in-out infinite;
235
+ }
236
+ /* Failed terminal state, actionable. Tappable affordance: clearer border,
237
+ pointer cursor, brightens on hover/focus to invite the retry tap. */
238
+ .mic[data-mic-state="none"] {
239
+ background: rgba(255,255,255,0.05);
240
+ border-color: rgba(255,255,255,0.14);
241
+ color: rgba(255,255,255,0.72);
242
+ cursor: pointer;
243
+ }
244
+ .mic[data-mic-state="none"]:hover {
245
+ background: rgba(255,255,255,0.12);
246
+ border-color: rgba(255,255,255,0.24);
247
+ color: #fff;
248
+ }
249
+ @keyframes micBreathe {
250
+ 0%, 100% { opacity: 0.55; }
251
+ 50% { opacity: 1; }
252
+ }
228
253
  .mic-icon { width: 13px; height: 13px; display: inline-block; flex-shrink: 0; }
229
254
  .mic-label { font-weight: 500; letter-spacing: 0.01em; white-space: nowrap; }
230
255
 
@@ -693,7 +718,10 @@ function micChipState(store) {
693
718
  if (store.indicatorState === "finishing" || store.indicatorState === "done" || store.indicatorState === "error") {
694
719
  return "inactive";
695
720
  }
696
- if (!store.hasMicPermission) return "none";
721
+ if (!store.hasMicPermission) {
722
+ if (store.micAcquiring) return "connecting";
723
+ return "none";
724
+ }
697
725
  return store.muted ? "muted" : "recording";
698
726
  }
699
727
  function renderIndicatorState(store) {
@@ -708,6 +736,8 @@ function renderIndicatorState(store) {
708
736
  dot.setAttribute("data-state", store.indicatorState);
709
737
  const chipState = micChipState(store);
710
738
  mic.setAttribute("data-mic-state", chipState === "inactive" ? "none" : chipState);
739
+ mic.removeAttribute("data-mic-fail");
740
+ if (chipState === "none") mic.setAttribute("data-mic-fail", store.micFailReason ?? "blocked");
711
741
  switch (store.indicatorState) {
712
742
  case "recording":
713
743
  case "no-audio":
@@ -742,13 +772,23 @@ function renderIndicatorState(store) {
742
772
  mic.setAttribute("aria-pressed", "true");
743
773
  mic.removeAttribute("tabindex");
744
774
  break;
745
- case "none":
746
- micIcon.innerHTML = MIC_MUTED_ICON_SVG;
747
- micLabel.textContent = "No mic, replay only";
748
- mic.setAttribute("aria-label", "Microphone not granted, replay only");
775
+ case "connecting":
776
+ micIcon.innerHTML = MIC_ICON_SVG;
777
+ micLabel.textContent = "Connecting mic";
778
+ mic.setAttribute("aria-label", "Connecting microphone");
749
779
  mic.setAttribute("aria-pressed", "false");
750
780
  mic.setAttribute("tabindex", "-1");
751
781
  break;
782
+ case "none": {
783
+ micIcon.innerHTML = MIC_MUTED_ICON_SVG;
784
+ const failLabel = store.micFailReason === "not-found" ? "No mic found, tap to retry" : "Mic blocked, tap to retry";
785
+ const failAria = store.micFailReason === "not-found" ? "No microphone found, tap to retry. Replay continues." : "Microphone blocked, tap to retry. Replay continues.";
786
+ micLabel.textContent = failLabel;
787
+ mic.setAttribute("aria-label", failAria);
788
+ mic.setAttribute("aria-pressed", "false");
789
+ mic.removeAttribute("tabindex");
790
+ break;
791
+ }
752
792
  case "inactive":
753
793
  micIcon.innerHTML = MIC_ICON_SVG;
754
794
  micLabel.textContent = store.indicatorState === "finishing" ? "Saving" : store.indicatorState === "done" ? "Saved" : "Save failed";
@@ -1327,8 +1367,13 @@ function enqueueChunk(store, ctx, blob) {
1327
1367
  });
1328
1368
  }
1329
1369
  async function startRecording(store, ctx) {
1370
+ store.micAcquiring = true;
1371
+ store.micFailReason = null;
1372
+ renderIndicatorState(store);
1330
1373
  if (!isMediaRecorderSupported()) {
1331
1374
  ctx.logger.warn("MediaRecorder not supported, continuing without audio");
1375
+ store.micAcquiring = false;
1376
+ store.micFailReason = "unsupported";
1332
1377
  store.indicatorState = "no-audio";
1333
1378
  renderIndicatorState(store);
1334
1379
  return;
@@ -1338,12 +1383,17 @@ async function startRecording(store, ctx) {
1338
1383
  stream = await navigator.mediaDevices.getUserMedia({ audio: true });
1339
1384
  } catch (err) {
1340
1385
  ctx.logger.warn("mic permission denied or unavailable", err);
1386
+ store.micAcquiring = false;
1387
+ const name = err instanceof Error ? err.name : "";
1388
+ store.micFailReason = name === "NotFoundError" || name === "DevicesNotFoundError" ? "not-found" : "blocked";
1341
1389
  store.indicatorState = "no-audio";
1342
1390
  renderIndicatorState(store);
1343
1391
  return;
1344
1392
  }
1345
1393
  store.stream = stream;
1346
1394
  store.hasMicPermission = true;
1395
+ store.micAcquiring = false;
1396
+ store.micFailReason = null;
1347
1397
  const mimeType = pickMimeType();
1348
1398
  let recorder;
1349
1399
  try {
@@ -1352,6 +1402,9 @@ async function startRecording(store, ctx) {
1352
1402
  ctx.logger.error("MediaRecorder construction failed", err);
1353
1403
  stream.getTracks().forEach((t) => t.stop());
1354
1404
  store.stream = null;
1405
+ store.hasMicPermission = false;
1406
+ store.micAcquiring = false;
1407
+ store.micFailReason = "unsupported";
1355
1408
  store.indicatorState = "no-audio";
1356
1409
  renderIndicatorState(store);
1357
1410
  return;
@@ -1366,6 +1419,10 @@ async function startRecording(store, ctx) {
1366
1419
  ctx.logger.error("MediaRecorder error", event);
1367
1420
  });
1368
1421
  recorder.start(store.options.chunkSeconds * 1e3);
1422
+ if (store.indicatorState === "no-audio") {
1423
+ store.indicatorState = "recording";
1424
+ }
1425
+ renderIndicatorState(store);
1369
1426
  }
1370
1427
  function toggleMute(store) {
1371
1428
  if (!store.stream || !store.hasMicPermission) return false;
@@ -1521,6 +1578,8 @@ function userTest(options = {}) {
1521
1578
  outsidePointerHandler: null,
1522
1579
  keydownHandler: null,
1523
1580
  hasMicPermission: false,
1581
+ micAcquiring: true,
1582
+ micFailReason: null,
1524
1583
  muted: false,
1525
1584
  mutedSinceMs: null,
1526
1585
  mutedSegments: [],
@@ -1545,7 +1604,12 @@ function userTest(options = {}) {
1545
1604
  };
1546
1605
  const onToggleTasks = () => setPanelOpen(!store.tasksPanelOpen);
1547
1606
  const onToggleMute = () => {
1548
- if (!store.hasMicPermission) return;
1607
+ if (!store.hasMicPermission) {
1608
+ if (!store.micAcquiring && store.indicatorState !== "finishing" && store.indicatorState !== "done" && store.indicatorState !== "error") {
1609
+ void startRecording(store, ctx);
1610
+ }
1611
+ return;
1612
+ }
1549
1613
  const ok = toggleMute(store);
1550
1614
  if (!ok) return;
1551
1615
  if (store.muted) showMuteToast(store);
@@ -1674,7 +1738,7 @@ function userTest(options = {}) {
1674
1738
  }
1675
1739
  };
1676
1740
  }
1677
- var __test__ = { getTestSlug, pickMimeType, isMediaRecorderSupported };
1741
+ var __test__ = { getTestSlug, pickMimeType, isMediaRecorderSupported, micChipState };
1678
1742
 
1679
1743
  exports.__test__ = __test__;
1680
1744
  exports.userTest = userTest;