feat(mobile): autocomplete redesign for touch surfaces

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.
This commit is contained in:
Bohdan Triapitsyn
2026-07-05 02:58:41 +03:00
parent da78de3a21
commit d762b69cec
6 changed files with 121 additions and 38 deletions
@@ -1,7 +1,9 @@
import React from 'react';
import { cn, fuzzyMatch } from '@/lib/utils';
import { useSkillsStore } from '@/stores/useSkillsStore';
import { useUIStore } from '@/stores/useUIStore';
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
import { useMobileAutocompleteMaxHeight } from './useMobileAutocompleteMaxHeight';
interface SkillInfo {
name: string;
@@ -28,6 +30,8 @@ export const SkillAutocomplete = React.forwardRef<SkillAutocompleteHandle, Skill
style,
}, ref) => {
const containerRef = React.useRef<HTMLDivElement | null>(null);
const isMobile = useUIStore((state) => state.isMobile);
const mobileMaxHeight = useMobileAutocompleteMaxHeight(containerRef, isMobile);
const [selectedIndex, setSelectedIndex] = React.useState(0);
const selectedIndexRef = React.useRef(0);
const keyboardNavigationRef = React.useRef(false);
@@ -128,7 +132,8 @@ export const SkillAutocomplete = React.forwardRef<SkillAutocompleteHandle, Skill
itemRefs.current[index] = el;
}}
className={cn(
'flex items-start gap-2 px-3 py-1.5 cursor-pointer rounded-lg typography-ui-label',
'flex gap-2 px-3 py-1.5 cursor-pointer rounded-lg typography-ui-label',
isMobile ? 'items-center' : 'items-start',
index === selectedIndex && 'bg-interactive-selection'
)}
onClick={() => onSkillSelect(skill.name)}
@@ -152,7 +157,7 @@ export const SkillAutocomplete = React.forwardRef<SkillAutocompleteHandle, Skill
{source}
</span>
</div>
{skill.description && (
{skill.description && !isMobile && (
<div className="typography-meta text-muted-foreground mt-0.5 truncate">
{skill.description}
</div>
@@ -166,9 +171,9 @@ export const SkillAutocomplete = React.forwardRef<SkillAutocompleteHandle, Skill
<div
ref={containerRef}
className="absolute z-[100] min-w-0 w-full max-w-[450px] max-h-60 bg-background border-2 border-border/60 rounded-xl shadow-none bottom-full mb-2 left-0 flex flex-col"
style={style}
style={mobileMaxHeight !== undefined ? { ...style, maxHeight: mobileMaxHeight } : style}
>
<ScrollableOverlay outerClassName="flex-1 min-h-0" className="px-0 pb-2">
<ScrollableOverlay preventOverscroll outerClassName="flex-1 min-h-0" className="px-0 pb-2">
{filteredSkills.length ? (
<div>
{filteredSkills.map((skill, index) => renderSkill(skill, index))}
@@ -179,9 +184,11 @@ export const SkillAutocomplete = React.forwardRef<SkillAutocompleteHandle, Skill
</div>
)}
</ScrollableOverlay>
<div className="px-3 pt-1 pb-1.5 border-t typography-meta text-muted-foreground">
navigate Enter select Esc close
</div>
{!isMobile && (
<div className="px-3 pt-1 pb-1.5 border-t typography-meta text-muted-foreground">
navigate Enter select Esc close
</div>
)}
</div>
);
});