@usero/sdk 1.1.12 → 1.1.13

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.
@@ -93,6 +93,7 @@ interface RecorderStore {
93
93
  tasksPanelOpen: boolean;
94
94
  outsidePointerHandler: ((event: PointerEvent) => void) | null;
95
95
  keydownHandler: ((event: KeyboardEvent) => void) | null;
96
+ keyboardWatcherCleanup: (() => void) | null;
96
97
  hasMicPermission: boolean;
97
98
  micAcquiring: boolean;
98
99
  micFailReason: 'blocked' | 'not-found' | 'unsupported' | null;
@@ -153,6 +154,7 @@ declare function persistActiveSession(store: RecorderStore, status: 'active' | '
153
154
  declare function getTestSlug(queryParam: string): string | null;
154
155
  declare function adoptSession(apiUrl: string, sessionId: string): Promise<AdoptResult>;
155
156
 
157
+ declare function computeKeyboardInset(innerHeight: number, viewportHeight: number, viewportOffsetTop: number): number;
156
158
  declare function micChipState(store: RecorderStore): 'recording' | 'muted' | 'none' | 'connecting' | 'silent' | 'inactive';
157
159
 
158
160
  declare function userTest(options?: UserTestOptions): UseroPlugin;
@@ -163,6 +165,7 @@ declare const __test__: {
163
165
  classifyChunkResponse: typeof classifyChunkResponse;
164
166
  handleSessionClosed: typeof handleSessionClosed;
165
167
  micChipState: typeof micChipState;
168
+ computeKeyboardInset: typeof computeKeyboardInset;
166
169
  isStreamSilent: typeof isStreamSilent;
167
170
  rmsDbFromSamples: typeof rmsDbFromSamples;
168
171
  SILENCE_RMS_DB_THRESHOLD: number;
@@ -93,6 +93,7 @@ interface RecorderStore {
93
93
  tasksPanelOpen: boolean;
94
94
  outsidePointerHandler: ((event: PointerEvent) => void) | null;
95
95
  keydownHandler: ((event: KeyboardEvent) => void) | null;
96
+ keyboardWatcherCleanup: (() => void) | null;
96
97
  hasMicPermission: boolean;
97
98
  micAcquiring: boolean;
98
99
  micFailReason: 'blocked' | 'not-found' | 'unsupported' | null;
@@ -153,6 +154,7 @@ declare function persistActiveSession(store: RecorderStore, status: 'active' | '
153
154
  declare function getTestSlug(queryParam: string): string | null;
154
155
  declare function adoptSession(apiUrl: string, sessionId: string): Promise<AdoptResult>;
155
156
 
157
+ declare function computeKeyboardInset(innerHeight: number, viewportHeight: number, viewportOffsetTop: number): number;
156
158
  declare function micChipState(store: RecorderStore): 'recording' | 'muted' | 'none' | 'connecting' | 'silent' | 'inactive';
157
159
 
158
160
  declare function userTest(options?: UserTestOptions): UseroPlugin;
@@ -163,6 +165,7 @@ declare const __test__: {
163
165
  classifyChunkResponse: typeof classifyChunkResponse;
164
166
  handleSessionClosed: typeof handleSessionClosed;
165
167
  micChipState: typeof micChipState;
168
+ computeKeyboardInset: typeof computeKeyboardInset;
166
169
  isStreamSilent: typeof isStreamSilent;
167
170
  rmsDbFromSamples: typeof rmsDbFromSamples;
168
171
  SILENCE_RMS_DB_THRESHOLD: number;
@@ -291,19 +291,77 @@ async function postNoteWithRetry(apiUrl, sessionId, atMs, text, logger) {
291
291
  }
292
292
 
293
293
  // src/plugins/user-test/ui.ts
294
+ var KEYBOARD_OPEN_MIN_INSET_PX = 80;
295
+ function computeKeyboardInset(innerHeight, viewportHeight, viewportOffsetTop) {
296
+ return Math.max(0, Math.round(innerHeight - (viewportHeight + viewportOffsetTop)));
297
+ }
298
+ function installKeyboardInsetWatcher(anchor) {
299
+ const viewport = window.visualViewport;
300
+ if (!viewport) return null;
301
+ let rafId = 0;
302
+ let lastInset = -1;
303
+ let resizeSeen = false;
304
+ const apply = () => {
305
+ rafId = 0;
306
+ const fromResize = resizeSeen;
307
+ resizeSeen = false;
308
+ const inset = computeKeyboardInset(window.innerHeight, viewport.height, viewport.offsetTop);
309
+ if (inset === lastInset) return;
310
+ lastInset = inset;
311
+ anchor.setAttribute("data-vv-scrolling", fromResize ? "false" : "true");
312
+ anchor.style.setProperty("--keyboard-inset", `${inset}px`);
313
+ anchor.style.setProperty("--vv-height", `${Math.round(viewport.height)}px`);
314
+ anchor.setAttribute("data-keyboard-open", inset >= KEYBOARD_OPEN_MIN_INSET_PX ? "true" : "false");
315
+ };
316
+ const schedule = () => {
317
+ if (rafId === 0) rafId = window.requestAnimationFrame(apply);
318
+ };
319
+ const onResize = () => {
320
+ resizeSeen = true;
321
+ schedule();
322
+ };
323
+ const onScroll = () => {
324
+ schedule();
325
+ };
326
+ viewport.addEventListener("resize", onResize);
327
+ viewport.addEventListener("scroll", onScroll);
328
+ resizeSeen = true;
329
+ apply();
330
+ return () => {
331
+ viewport.removeEventListener("resize", onResize);
332
+ viewport.removeEventListener("scroll", onScroll);
333
+ if (rafId !== 0) window.cancelAnimationFrame(rafId);
334
+ };
335
+ }
294
336
  function buildIndicator(host, store, callbacks) {
295
337
  const root = host.attachShadow({ mode: "open" });
296
338
  const style = document.createElement("style");
297
339
  style.textContent = `
298
340
  :host { all: initial; }
299
341
  .anchor {
342
+ /* --keyboard-inset is the height of the mobile soft keyboard, written by
343
+ the visualViewport watcher (0 when closed / unsupported). position:fixed
344
+ anchors to the LAYOUT viewport, which the keyboard does not shrink, so
345
+ without this the open keyboard covers the bar and task panel entirely. */
346
+ --keyboard-inset: 0px;
300
347
  position: fixed;
301
- bottom: calc(env(safe-area-inset-bottom, 0px) + 16px);
348
+ bottom: calc(env(safe-area-inset-bottom, 0px) + 16px + var(--keyboard-inset));
302
349
  left: 50%; transform: translateX(-50%);
303
350
  display: flex; flex-direction: column; align-items: center; gap: 8px;
304
351
  z-index: 2147483646; max-width: calc(100vw - 32px);
305
352
  font: 13px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
306
353
  color: #fff;
354
+ /* Eased only for keyboard show/hide; scroll ticks suppress it below. */
355
+ transition: bottom 0.18s ease-out;
356
+ }
357
+ /* While the visual viewport is being panned (URL bar collapse, pinch,
358
+ keyboard-driven scroll) the inset must track 1:1, animating every tick
359
+ reads as lag. The watcher flips this attribute per update source. */
360
+ .anchor[data-vv-scrolling="true"] { transition: none; }
361
+ /* Keyboard open: the keyboard covers the home-indicator zone, so the
362
+ safe-area + 16px margin is dead space. Tuck in to an 8px gap instead. */
363
+ .anchor[data-keyboard-open="true"] {
364
+ bottom: calc(var(--keyboard-inset) + 8px);
307
365
  }
308
366
  .bar {
309
367
  display: inline-flex; align-items: center; gap: 6px;
@@ -326,6 +384,16 @@ function buildIndicator(host, store, callbacks) {
326
384
  width: max-content; overflow-y: auto;
327
385
  }
328
386
  .panel[hidden] { display: none; }
387
+ /* Compact state while the keyboard is up: 60vh is measured against the
388
+ LAYOUT viewport and can exceed the visible strip above the keyboard,
389
+ clipping the instructions. Cap against the VISUAL viewport height
390
+ (--vv-height, written by the watcher) minus the bar's footprint, with a
391
+ 96px floor so at least a couple of lines stay readable and scrollable.
392
+ Slightly tighter padding to make the most of the scarce space. */
393
+ .anchor[data-keyboard-open="true"] .panel {
394
+ max-height: min(480px, max(96px, calc(var(--vv-height, 100vh) - 96px)));
395
+ padding: 10px 12px 10px 8px;
396
+ }
329
397
  .panel ol { margin: 0; padding-left: 26px; }
330
398
  .panel li { margin: 0 0 8px; }
331
399
  .panel li:last-child { margin: 0; }
@@ -761,10 +829,12 @@ function buildIndicator(host, store, callbacks) {
761
829
  .dot { animation: none; }
762
830
  .toast, .note-popover, .resume-toast { animation: none; }
763
831
  .resume-toast[data-leaving="true"] { opacity: 0; }
832
+ .anchor { transition: none; }
764
833
  }
765
834
  `;
766
835
  const anchor = document.createElement("div");
767
836
  anchor.className = "anchor";
837
+ store.keyboardWatcherCleanup = installKeyboardInsetWatcher(anchor);
768
838
  const panel = document.createElement("div");
769
839
  panel.className = "panel";
770
840
  panel.hidden = true;
@@ -1921,6 +1991,7 @@ function userTest(options = {}) {
1921
1991
  tasksPanelOpen: readTasksPanelOpen(),
1922
1992
  outsidePointerHandler: null,
1923
1993
  keydownHandler: null,
1994
+ keyboardWatcherCleanup: null,
1924
1995
  hasMicPermission: false,
1925
1996
  micAcquiring: true,
1926
1997
  micFailReason: null,
@@ -2127,6 +2198,10 @@ function userTest(options = {}) {
2127
2198
  document.removeEventListener("keydown", store.keydownHandler);
2128
2199
  store.keydownHandler = null;
2129
2200
  }
2201
+ if (store.keyboardWatcherCleanup) {
2202
+ store.keyboardWatcherCleanup();
2203
+ store.keyboardWatcherCleanup = null;
2204
+ }
2130
2205
  for (const id of store.muteToastTimers) {
2131
2206
  try {
2132
2207
  window.clearTimeout(id);
@@ -2156,6 +2231,7 @@ var __test__ = {
2156
2231
  classifyChunkResponse,
2157
2232
  handleSessionClosed,
2158
2233
  micChipState,
2234
+ computeKeyboardInset,
2159
2235
  isStreamSilent,
2160
2236
  rmsDbFromSamples,
2161
2237
  SILENCE_RMS_DB_THRESHOLD,