Skip to content

Commit dced0b3

Browse files
Add files via upload
1 parent c8efbbd commit dced0b3

2 files changed

Lines changed: 180 additions & 44 deletions

File tree

‎dev/_static/ai-assistant.css‎

Lines changed: 74 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -965,6 +965,12 @@
965965
room left on that line and wraps onto its own row underneath it. */
966966
flex-wrap: wrap;
967967
padding: 0.625rem 0.75rem;
968+
/* Run 84: when the panel sits edge-to-edge on mobile/landscape, keep the
969+
* composer controls clear of notches and the home indicator. Desktop env()
970+
* values are zero, so these preserve the existing spacing there. */
971+
padding-left: max(0.75rem, env(safe-area-inset-left, 0px));
972+
padding-right: max(0.75rem, env(safe-area-inset-right, 0px));
973+
padding-bottom: max(0.625rem, env(safe-area-inset-bottom, 0px));
968974
border-top: 1px solid var(
969975
--pst-color-border,
970976
var(--color-foreground-border, #e1e4e5)
@@ -3515,12 +3521,9 @@ body.ai-assistant-resizing-y .ai-assistant-panel-resizer-top { opacity: 1; }
35153521
height: 0.95rem;
35163522
}
35173523

3518-
/* On narrow viewports collapse the right-cluster labels — keep icons only.
3519-
* Hamburger preserves access to every entry-point.
3520-
*
3521-
* Also collapses the footer inline model picker from its full pill form
3522-
* (dot + label + chevron) to a single model icon — same breakpoint, same
3523-
* logic as .ai-assistant-panel-model-link-label above. */
3524+
/* On narrow viewports collapse the sub-bar right-cluster labels — keep icons
3525+
* only. The COMPOSER below deliberately does not use this viewport breakpoint:
3526+
* Run 84 makes its model picker panel-fit-owned on phone, tablet, and desktop. */
35243527
@media (max-width: 575px) {
35253528
/* Sub-bar right-cluster: hide text labels, keep icons.
35263529
* ep-btn-label is excluded — it manages its own collapse via [data-narrow]. */
@@ -3529,38 +3532,16 @@ body.ai-assistant-resizing-y .ai-assistant-panel-resizer-top { opacity: 1; }
35293532
.ai-assistant-panel-share-link span:not([aria-hidden]) {
35303533
display: none;
35313534
}
3532-
3533-
/* Footer inline model picker: collapse pill → bare icon button.
3534-
* Reset the pill chrome (border, min-width, padding) so the button
3535-
* looks and feels like the other icon-only footer buttons (mic, send). */
3536-
.ai-assistant-panel-inline-model-picker {
3537-
min-width: unset;
3538-
max-width: unset;
3539-
border: none;
3540-
border-radius: 6px;
3541-
padding: 0.35rem 0.4rem; /* matches .ai-assistant-panel-footer-btn */
3542-
background: transparent;
3543-
}
3544-
/* Show the model SVG icon (hidden by default above) */
3545-
.ai-assistant-panel-inline-picker-icon {
3546-
display: inline-flex;
3547-
}
3548-
/* Hide the pill-only elements: dot, label, chevron */
3549-
.ai-assistant-panel-inline-picker-dot,
3550-
.ai-assistant-panel-inline-picker-label,
3551-
.ai-assistant-panel-inline-picker-chev {
3552-
display: none;
3553-
}
35543535
}
35553536

35563537

3557-
/* Run 83 — panel-owned footer contraction.
3538+
/* Run 83/84 — panel-owned footer contraction.
35583539
*
3559-
* The mobile @media fallback above remains for old browsers and preserves the
3560-
* existing phone/tablet appearance. Modern browsers additionally receive the
3561-
* fit-based data-footer-compact state from the panel ResizeObserver, so a
3562-
* manually narrowed desktop panel behaves exactly like the same-width panel on
3563-
* a phone. The picker contracts atomically; attach/mic/send stay stable. */
3540+
* data-footer-compact is the single model-picker representation authority on
3541+
* every device. A roomy landscape phone/tablet may therefore keep the full
3542+
* pill, while a manually narrowed desktop panel uses the icon-only form.
3543+
* Initial fit runs after DOM connection; ResizeObserver (or the Run 84 window/
3544+
* visualViewport fallback) owns subsequent changes. */
35643545
.ai-assistant-panel[data-footer-compact="true"] .ai-assistant-panel-inline-model-picker {
35653546
min-width: unset;
35663547
max-width: unset;
@@ -5759,6 +5740,44 @@ details.ai-md-section[open] > summary .ai-md-section-chevron {
57595740
transform: rotate(180deg);
57605741
}
57615742

5743+
/* Run 84 — touch/coarse-pointer composer contract.
5744+
* Touch has no reliable hover discovery, so microphone options must be visible
5745+
* without hover. Footer controls also receive dense-but-usable 40px targets;
5746+
* the panel-fit controller measures their REAL sizes, so this does not re-open
5747+
* the overflow bug fixed in Run 83. The desktop hover bridge is disabled on
5748+
* touch because it would otherwise create an invisible hit strip above the mic. */
5749+
@media (hover: none), (pointer: coarse) {
5750+
.ai-assistant-panel-footer-btn,
5751+
.ai-assistant-panel-inline-model-picker {
5752+
min-width: 2.5rem;
5753+
min-height: 2.5rem;
5754+
}
5755+
.ai-assistant-panel-footer-btn--attach,
5756+
.ai-assistant-panel-footer-btn--send {
5757+
width: 2.5rem;
5758+
height: 2.5rem;
5759+
}
5760+
.ai-assistant-mic-expand-wrapper,
5761+
.ai-assistant-mic-wrapper:hover .ai-assistant-mic-expand-wrapper {
5762+
width: 2.5rem;
5763+
}
5764+
.ai-assistant-mic-expand-btn {
5765+
width: 2.5rem;
5766+
height: 2.5rem;
5767+
opacity: 1;
5768+
}
5769+
.ai-assistant-mic-wrapper::before {
5770+
display: none;
5771+
}
5772+
.ai-assistant-mic-popup-row--hold,
5773+
.ai-assistant-panel-attach-menu-item {
5774+
min-height: 44px;
5775+
}
5776+
.ai-assistant-panel-attach-menu-shortcut {
5777+
display: none;
5778+
}
5779+
}
5780+
57625781
/* ─── Popup container ────────────────────────────────────────────────────── */
57635782
/*
57645783
* bottom: calc(100% + 0.45rem) — floats above the action bar with a gap.
@@ -5777,7 +5796,17 @@ details.ai-md-section[open] > summary .ai-md-section-chevron {
57775796
flex-direction: column;
57785797
gap: 0;
57795798

5780-
min-width: 330px; /* 100 bars×2px + 99 gaps×1px = 299px; net content ≈ 303px after padding */
5799+
/* Run 84: 330px remains the roomy target, but JS supplies a panel-
5800+
* bounded inline width for narrow phones/tablets/resized desktop panels.
5801+
* The viewport max is a defensive no-JS/early-paint fallback. */
5802+
width: min(330px, calc(100vw - 1rem));
5803+
min-width: 0;
5804+
max-width: calc(100vw - 1rem);
5805+
max-height: calc(100dvh - 1rem);
5806+
overflow-x: hidden;
5807+
overflow-y: auto;
5808+
overscroll-behavior: contain;
5809+
scrollbar-width: thin;
57815810
padding: 0.3rem;
57825811

57835812
background: var(
@@ -5914,9 +5943,17 @@ details.ai-md-section[open] > summary .ai-md-section-chevron {
59145943
display: flex;
59155944
align-items: flex-end;
59165945
gap: 1px;
5946+
width: 100%;
5947+
min-width: 0;
59175948
height: 24px;
5918-
/* 100 bars × 2px + 99 gaps × 1px = 299px — popup min-width is 330px (≈ 303px net) */
5919-
overflow: hidden; /* clip any sub-pixel overflow on fractional viewports */
5949+
/* 100 bars × 2px + 99 gaps × 1px = 299px at the roomy size. */
5950+
overflow: hidden; /* clip only fractional rounding overflow */
5951+
}
5952+
/* Preserve all 100 VU zones in a panel-bounded popup instead of clipping the
5953+
* high-level red zone off the right edge. 0.5px gaps reduce the full strip
5954+
* to ~249.5px, which fits the 300px minimum panel after popup padding. */
5955+
.ai-assistant-mic-popup[data-compact="true"] .ai-assistant-mic-level-bars {
5956+
gap: 0.5px;
59205957
}
59215958

59225959
/* ─── Individual bar: JS-driven height from AnalyserNode, smooth transition ─── */

‎dev/_static/ai-assistant.js‎

Lines changed: 106 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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 = '';
@@ -31241,6 +31259,10 @@
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');
@@ -31350,7 +31372,9 @@
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';
@@ -31360,7 +31384,7 @@
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 =
@@ -31370,6 +31394,50 @@
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
@@ -31383,6 +31451,7 @@
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);
@@ -32049,6 +32118,7 @@
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
@@ -32140,6 +32210,35 @@
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

Comments
 (0)