@@ -31029,19 +31029,37 @@
3102931029 function _positionAttachMenu() {
3103031030 var bodyRect = null;
3103131031 var buttonRect = null;
31032+ var wrapRect = null;
3103231033 try {
3103331034 var panelBody = document.getElementById('ai-assistant-panel-body');
3103431035 bodyRect = panelBody && panelBody.getBoundingClientRect ? panelBody.getBoundingClientRect() : null;
3103531036 buttonRect = attachBtn.getBoundingClientRect ? attachBtn.getBoundingClientRect() : null;
31037+ wrapRect = attachWrap.getBoundingClientRect ? attachWrap.getBoundingClientRect() : null;
3103631038 } catch (_e) {}
31037- if (bodyRect && buttonRect) {
31039+ if (bodyRect && buttonRect && wrapRect ) {
3103831040 var available = Math.max(112, Math.floor(buttonRect.top - bodyRect.top - 10));
31039- var bodyWidth = Math.max(180, Math.floor(Number(bodyRect.width) || 0));
31041+ // Run 84: body width alone is not a safe menu width because the
31042+ // menu is offset slightly left of the + trigger. On a 300–320px
31043+ // panel that old calculation could put the right edge several
31044+ // pixels outside the panel. Resolve the real local left offset,
31045+ // clamp it inside the body, then derive width from the remaining
31046+ // right-side capacity. This works identically on phone, tablet,
31047+ // and a manually narrowed desktop panel.
31048+ var menuStyle = window.getComputedStyle ? window.getComputedStyle(attachMenu) : null;
31049+ var cssLeft = menuStyle ? parseFloat(menuStyle.left) : 0;
31050+ if (!Number.isFinite(cssLeft)) cssLeft = 0;
31051+ var edgeInset = 6;
31052+ var localLeft = Math.max(cssLeft, bodyRect.left + edgeInset - wrapRect.left);
31053+ var rightBudget = Math.max(180,
31054+ Math.floor(bodyRect.right - edgeInset - (wrapRect.left + localLeft)));
31055+ var menuWidth = Math.min(324, rightBudget);
31056+ attachMenu.style.left = localLeft + 'px';
3104031057 attachMenu.style.maxHeight = available + 'px';
31041- attachMenu.style.width = Math.min(324, bodyWidth) + 'px';
31042- attachMenu.style.maxWidth = bodyWidth + 'px';
31043- attachMenu.style.minWidth = Math.min(272, bodyWidth ) + 'px';
31058+ attachMenu.style.width = menuWidth + 'px';
31059+ attachMenu.style.maxWidth = menuWidth + 'px';
31060+ attachMenu.style.minWidth = Math.min(272, menuWidth ) + 'px';
3104431061 } else {
31062+ attachMenu.style.left = '';
3104531063 attachMenu.style.maxHeight = '18rem';
3104631064 attachMenu.style.width = '';
3104731065 attachMenu.style.maxWidth = '';
3124131259 // Hold-to-record: when _micHoldMode is true, pointerdown/up drives recognition
3124231260 // instead of click-toggle — matching the Claude.ai "press and hold" pattern.
3124331261 var micBtnEl = null;
31262+ // Run 84: popup fitting is assigned when speech UI exists and invoked
31263+ // from the shared panel ResizeObserver via _syncFooterActionFit().
31264+ // A no-op keeps the later fit path branch-free when speech is absent.
31265+ var _syncMicPopupFit = function () {};
3124431266 if (hasSpeech) {
3124531267 // ── Wrapper ───────────────────────────────────────────────────────
3124631268 var micWrapper = document.createElement('div');
3135031372
3135131373 var micExpandWrapper = document.createElement('div');
3135231374 micExpandWrapper.className = 'ai-assistant-mic-expand-wrapper';
31353- micExpandWrapper.setAttribute('aria-hidden', 'true'); // wrapper is decorative
31375+ // Do NOT put aria-hidden on this wrapper: it owns a focusable button.
31376+ // aria-hidden on an ancestor suppresses the entire subtree, and a
31377+ // child cannot opt back into the accessibility tree.
3135431378
3135531379 var micExpandBtn = document.createElement('button');
3135631380 micExpandBtn.className = 'ai-assistant-mic-expand-btn';
3136031384 micExpandBtn.setAttribute('aria-haspopup', 'true');
3136131385 micExpandBtn.setAttribute('aria-expanded', 'false');
3136231386 micExpandBtn.setAttribute('aria-controls', 'ai-assistant-mic-popup');
31363- micExpandBtn.removeAttribute('aria-hidden'); // focusable — override wrapper
31387+ micExpandBtn.removeAttribute('aria-hidden'); // explicit: trigger remains exposed
3136431388
3136531389 // Chevron-up SVG: points up (popup appears above); rotates on open
3136631390 micExpandBtn.innerHTML =
3137031394 + '<polyline points="18 15 12 9 6 15"/>'
3137131395 + '</svg>';
3137231396
31397+ // Run 84 — panel-bounded microphone options surface.
31398+ // The legacy popup used min-width:330px and right:0 relative to the
31399+ // mic wrapper. That is fine on a roomy panel but can escape both a
31400+ // 320px phone and a 300px manually resized desktop panel. Fit the
31401+ // popup to the panel BODY rather than the viewport and use the space
31402+ // to the right of the mic (including Send) when centring its right
31403+ // edge. Vertical capacity is likewise bounded above the composer so
31404+ // short landscape/tablet layouts scroll the popup instead of pushing
31405+ // controls off-screen.
31406+ _syncMicPopupFit = function () {
31407+ if (!micPopup || !micWrapper || !panel ||
31408+ !micPopup.getBoundingClientRect || !micWrapper.getBoundingClientRect) return;
31409+ var panelBody = document.getElementById('ai-assistant-panel-body');
31410+ var panelRect = panel.getBoundingClientRect ? panel.getBoundingClientRect() : null;
31411+ var bodyRect = panelBody && panelBody.getBoundingClientRect
31412+ ? panelBody.getBoundingClientRect() : panelRect;
31413+ var wrapperRect = micWrapper.getBoundingClientRect();
31414+ if (!panelRect || !bodyRect || !wrapperRect) return;
31415+
31416+ var edgeInset = 8;
31417+ var boundaryLeft = Math.max(panelRect.left, bodyRect.left) + edgeInset;
31418+ var boundaryRight = Math.min(panelRect.right, bodyRect.right) - edgeInset;
31419+ var boundaryWidth = Math.max(1, boundaryRight - boundaryLeft);
31420+ var popupWidth = Math.min(330, Math.floor(boundaryWidth));
31421+ micPopup.style.width = popupWidth + 'px';
31422+ micPopup.style.minWidth = '0';
31423+ micPopup.style.maxWidth = popupWidth + 'px';
31424+ micPopup.setAttribute('data-compact', popupWidth < 315 ? 'true' : 'false');
31425+
31426+ // Preserve explicit drag coordinates until the popup is closed;
31427+ // natural anchored popups may use the free space to the right of
31428+ // the mic so their right edge stays inside the panel body.
31429+ if (micPopup.getAttribute('data-dragged') !== 'true') {
31430+ var targetRight = boundaryRight;
31431+ micPopup.style.right = (wrapperRect.right - targetRight) + 'px';
31432+ micPopup.style.left = '';
31433+ }
31434+
31435+ var availableHeight = Math.floor(wrapperRect.top - bodyRect.top - 10);
31436+ if (availableHeight > 0) {
31437+ micPopup.style.maxHeight = Math.max(96, availableHeight) + 'px';
31438+ }
31439+ };
31440+
3137331441 // ── Popup ownership helpers ────────────────────────────────────────
3137431442 //
3137531443 // data-pinned is the single visibility authority. Keep all writes
3138331451 }
3138431452 if (open && explicit === true) _micPopupExplicitPinned = true;
3138531453 if (!open) _micPopupExplicitPinned = false;
31454+ if (open) _syncMicPopupFit();
3138631455 var value = open ? 'true' : 'false';
3138731456 micPopup.setAttribute('data-pinned', value);
3138831457 micExpandBtn.setAttribute('aria-expanded', value);
3204932118 ? actionsWidth < Math.ceil(required + _FOOTER_FIT_HYSTERESIS_PX)
3205032119 : actionsWidth < Math.ceil(required);
3205132120 panel.setAttribute('data-footer-compact', compact ? 'true' : 'false');
32121+ _syncMicPopupFit();
3205232122 }
3205332123
3205432124 // ResizeObserver: toggle data-narrow on the panel root when the panel
3214032210 _updateSubbarOverflow(w);
3214132211 });
3214232212 _subbarRO.observe(panel);
32213+ } else {
32214+ // Run 84 fallback: the composer is JS-generated, so a viewport CSS
32215+ // breakpoint is not needed to emulate panel responsiveness. Older
32216+ // engines without ResizeObserver still receive panel-fit updates on
32217+ // window/orientation/visual-viewport changes. This keeps one layout
32218+ // authority instead of allowing mobile CSS and panel JS to disagree.
32219+ var _footerFallbackRaf = null;
32220+ function _fallbackResponsiveSync() {
32221+ if (!panel.isConnected) {
32222+ window.removeEventListener('resize', _fallbackResponsiveSync);
32223+ if (window.visualViewport) {
32224+ window.visualViewport.removeEventListener('resize', _fallbackResponsiveSync);
32225+ }
32226+ return;
32227+ }
32228+ if (_footerFallbackRaf !== null) return;
32229+ var fallbackRaf = window.requestAnimationFrame || function (cb) { return setTimeout(cb, 16); };
32230+ _footerFallbackRaf = fallbackRaf(function () {
32231+ _footerFallbackRaf = null;
32232+ var rect = panel.getBoundingClientRect ? panel.getBoundingClientRect() : null;
32233+ var w = rect && rect.width ? rect.width : 0;
32234+ _syncFooterActionFit(w);
32235+ _syncSheetHeaderOverflow(w);
32236+ });
32237+ }
32238+ window.addEventListener('resize', _fallbackResponsiveSync, { passive: true });
32239+ if (window.visualViewport) {
32240+ window.visualViewport.addEventListener('resize', _fallbackResponsiveSync, { passive: true });
32241+ }
3214332242 }
3214432243
3214532244 // ── Events ────────────────────────────────────────────────────────────
0 commit comments