Command, file/agent, skill, and snippet autocompletes now stop at the top of the chat area (Capacitor) or the visible viewport edge (mobile browsers, which pan the page for the keyboard) and may grow that far, measured live across keyboard settles and viewport changes. On mobile the keyboard-hint footer and description lines are gone, rows center their icons, and list overscroll no longer bounces the page behind. Selecting a command no longer dismisses the keyboard (its rows now block the tap's focus steal like the composer buttons), and the dead dismissKeyboard option is removed.
58 lines
2.7 KiB
TypeScript
58 lines
2.7 KiB
TypeScript
import React from 'react';
|
|
|
|
/**
|
|
* Mobile: clamp an autocomplete popup (anchored above the composer via
|
|
* `bottom-full`) so it never rises past the top of the chat area. The chat
|
|
* `<main>` starts below the app header in both the Capacitor shell and the
|
|
* mobile browser, so its top edge is the correct boundary for both.
|
|
*
|
|
* Re-measures on window resizes and when the native keyboard choreography
|
|
* settles (the composer — and therefore the popup's anchor — moves with it).
|
|
*
|
|
* Returns an inline max-height in px, or undefined when disabled. NOTE: the
|
|
* inline value REPLACES any `max-h-*` class (it does not combine) — on mobile
|
|
* the popup is allowed to grow all the way to the boundary, unlike the
|
|
* desktop design cap.
|
|
*/
|
|
export const useMobileAutocompleteMaxHeight = (
|
|
containerRef: React.RefObject<HTMLElement | null>,
|
|
enabled: boolean,
|
|
): number | undefined => {
|
|
const [maxHeight, setMaxHeight] = React.useState<number | undefined>(undefined);
|
|
|
|
React.useLayoutEffect(() => {
|
|
if (!enabled) return;
|
|
const measure = () => {
|
|
const el = containerRef.current;
|
|
if (!el) return;
|
|
const main = el.closest('main');
|
|
if (!main) return;
|
|
// Mobile browsers pan the page up to reveal the focused field, so
|
|
// <main>'s top can sit ABOVE the visible screen (negative client
|
|
// coordinates). The binding boundary is whichever is lower: the
|
|
// chat area's top or the visual viewport's top (its offsetTop is
|
|
// expressed in the same layout-viewport client coordinates).
|
|
const visualTop = window.visualViewport?.offsetTop ?? 0;
|
|
const boundaryTop = Math.max(main.getBoundingClientRect().top, visualTop);
|
|
// The popup's bottom edge is its anchor (composer top) and does not
|
|
// depend on its current height.
|
|
const available = el.getBoundingClientRect().bottom - boundaryTop - 8;
|
|
const next = Math.max(120, Math.floor(available));
|
|
setMaxHeight((prev) => (prev === next ? prev : next));
|
|
};
|
|
measure();
|
|
window.addEventListener('resize', measure);
|
|
window.addEventListener('oc:keyboard-settled', measure);
|
|
window.visualViewport?.addEventListener('resize', measure);
|
|
window.visualViewport?.addEventListener('scroll', measure);
|
|
return () => {
|
|
window.removeEventListener('resize', measure);
|
|
window.removeEventListener('oc:keyboard-settled', measure);
|
|
window.visualViewport?.removeEventListener('resize', measure);
|
|
window.visualViewport?.removeEventListener('scroll', measure);
|
|
};
|
|
});
|
|
|
|
return enabled ? maxHeight : undefined;
|
|
};
|