fix: simplify mobile composer keyboard handling
Use browser keyboard resizing by default on mobile Remove mobile-only autocomplete button and tabs Keep autocomplete behavior consistent across mobile and desktop
This commit is contained in:
@@ -470,14 +470,12 @@ const RevertedMessageDock: React.FC<RevertedMessageDockProps> = React.memo(({ se
|
||||
RevertedMessageDock.displayName = 'RevertedMessageDock';
|
||||
|
||||
type ComposerAttachmentControlsProps = {
|
||||
isMobile: boolean;
|
||||
isVSCode: boolean;
|
||||
footerIconButtonClass: string;
|
||||
iconSizeClass: string;
|
||||
fileInputRef: React.RefObject<HTMLInputElement | null>;
|
||||
handleLocalFileSelect: (event: React.ChangeEvent<HTMLInputElement>) => void | Promise<void>;
|
||||
handlePickLocalFiles: () => void;
|
||||
handleOpenCommandMenu: () => void;
|
||||
openIssuePicker: () => void;
|
||||
openPrPicker: () => void;
|
||||
onOpenSettings?: () => void;
|
||||
@@ -486,14 +484,12 @@ type ComposerAttachmentControlsProps = {
|
||||
const ComposerAttachmentControls = React.memo(function ComposerAttachmentControls(props: ComposerAttachmentControlsProps) {
|
||||
const { t } = useI18n();
|
||||
const {
|
||||
isMobile,
|
||||
isVSCode,
|
||||
footerIconButtonClass,
|
||||
iconSizeClass,
|
||||
fileInputRef,
|
||||
handleLocalFileSelect,
|
||||
handlePickLocalFiles,
|
||||
handleOpenCommandMenu,
|
||||
openIssuePicker,
|
||||
openPrPicker,
|
||||
onOpenSettings,
|
||||
@@ -501,27 +497,6 @@ const ComposerAttachmentControls = React.memo(function ComposerAttachmentControl
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
{isMobile ? (
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
footerIconButtonClass,
|
||||
'rounded-md',
|
||||
'hover:bg-interactive-hover/40'
|
||||
)}
|
||||
onPointerDownCapture={(event) => {
|
||||
if (event.pointerType === 'touch') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}}
|
||||
onClick={handleOpenCommandMenu}
|
||||
title={t('chat.chatInput.actions.commands')}
|
||||
aria-label={t('chat.chatInput.actions.commands')}
|
||||
>
|
||||
<Icon name="command" className={cn(iconSizeClass)} />
|
||||
</button>
|
||||
) : null}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
@@ -598,8 +573,7 @@ const ComposerAttachmentControls = React.memo(function ComposerAttachmentControl
|
||||
</div>
|
||||
);
|
||||
}, (prev, next) => (
|
||||
prev.isMobile === next.isMobile
|
||||
&& prev.isVSCode === next.isVSCode
|
||||
prev.isVSCode === next.isVSCode
|
||||
&& prev.footerIconButtonClass === next.footerIconButtonClass
|
||||
&& prev.iconSizeClass === next.iconSizeClass
|
||||
&& prev.onOpenSettings === next.onOpenSettings
|
||||
@@ -960,7 +934,6 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
const [mentionQuery, setMentionQuery] = React.useState('');
|
||||
const [showCommandAutocomplete, setShowCommandAutocomplete] = React.useState(false);
|
||||
const [commandQuery, setCommandQuery] = React.useState('');
|
||||
const [autocompleteTab, setAutocompleteTab] = React.useState<'commands' | 'agents' | 'files'>('commands');
|
||||
const [showSkillAutocomplete, setShowSkillAutocomplete] = React.useState(false);
|
||||
const [skillQuery, setSkillQuery] = React.useState('');
|
||||
const [showSnippetAutocomplete, setShowSnippetAutocomplete] = React.useState(false);
|
||||
@@ -2525,7 +2498,6 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
if (cursorPosition <= commandEnd && firstSpace === -1) {
|
||||
const commandText = value.substring(1, commandEnd);
|
||||
setCommandQuery(commandText);
|
||||
setAutocompleteTab('commands');
|
||||
setShowCommandAutocomplete(true);
|
||||
setShowFileMention(false);
|
||||
setShowSkillAutocomplete(false);
|
||||
@@ -2578,7 +2550,6 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
const isWordBoundary = !charBefore || /\s/.test(charBefore);
|
||||
if (isWordBoundary && !textAfterAt.includes(' ') && !textAfterAt.includes('\n')) {
|
||||
setMentionQuery(textAfterAt);
|
||||
setAutocompleteTab((current) => current === 'files' ? 'files' : 'agents');
|
||||
setShowFileMention(true);
|
||||
} else {
|
||||
setShowFileMention(false);
|
||||
@@ -2586,92 +2557,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
} else {
|
||||
setShowFileMention(false);
|
||||
}
|
||||
}, [inputMode, setAutocompleteTab, setCommandQuery, setMentionQuery, setShowCommandAutocomplete, setShowFileMention, setShowSkillAutocomplete, setSkillQuery]);
|
||||
|
||||
const applyAutocompletePrefix = React.useCallback((prefix: '/' | '@') => {
|
||||
const nextMessage = message.length === 0
|
||||
? prefix
|
||||
: (message[0] === '/' || message[0] === '@')
|
||||
? `${prefix}${message.slice(1)}`
|
||||
: `${prefix}${message}`;
|
||||
setMessage(nextMessage);
|
||||
requestAnimationFrame(() => {
|
||||
if (textareaRef.current) {
|
||||
const nextCursor = Math.min(nextMessage.length, textareaRef.current.value.length);
|
||||
textareaRef.current.selectionStart = nextCursor;
|
||||
textareaRef.current.selectionEnd = nextCursor;
|
||||
}
|
||||
adjustTextareaHeight();
|
||||
updateAutocompleteState(nextMessage, nextMessage.length);
|
||||
});
|
||||
}, [adjustTextareaHeight, message, setMessage, updateAutocompleteState]);
|
||||
|
||||
const handleAutocompleteTabSelect = React.useCallback((tab: 'commands' | 'agents' | 'files') => {
|
||||
const textarea = textareaRef.current;
|
||||
if (isMobile && textarea) {
|
||||
try {
|
||||
textarea.focus({ preventScroll: true });
|
||||
} catch {
|
||||
textarea.focus();
|
||||
}
|
||||
const len = textarea.value.length;
|
||||
try {
|
||||
textarea.setSelectionRange(len, len);
|
||||
} catch {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
const cursorPosition = textarea?.selectionStart ?? message.length;
|
||||
const textBeforeCursor = message.substring(0, cursorPosition);
|
||||
const lastAtSymbol = textBeforeCursor.lastIndexOf('@');
|
||||
const nextMentionQuery = lastAtSymbol !== -1
|
||||
? textBeforeCursor.substring(lastAtSymbol + 1).replace(/[\s\n].*$/, '')
|
||||
: '';
|
||||
|
||||
setAutocompleteTab(tab);
|
||||
setCommandQuery('');
|
||||
if (tab === 'commands') {
|
||||
setMentionQuery('');
|
||||
applyAutocompletePrefix('/');
|
||||
}
|
||||
if (tab === 'agents') {
|
||||
setMentionQuery(nextMentionQuery);
|
||||
applyAutocompletePrefix('@');
|
||||
}
|
||||
if (tab === 'files') {
|
||||
setMentionQuery(nextMentionQuery);
|
||||
applyAutocompletePrefix('@');
|
||||
}
|
||||
setShowSkillAutocomplete(false);
|
||||
setShowCommandAutocomplete(tab === 'commands');
|
||||
setShowFileMention(tab === 'agents' || tab === 'files');
|
||||
}, [applyAutocompletePrefix, isMobile, message, setAutocompleteTab, setCommandQuery, setMentionQuery, setShowCommandAutocomplete, setShowFileMention, setShowSkillAutocomplete]);
|
||||
|
||||
const handleOpenCommandMenu = React.useCallback(() => {
|
||||
if (!isMobile) {
|
||||
return;
|
||||
}
|
||||
const textarea = textareaRef.current;
|
||||
if (textarea) {
|
||||
try {
|
||||
textarea.focus({ preventScroll: true });
|
||||
} catch {
|
||||
textarea.focus();
|
||||
}
|
||||
const len = textarea.value.length;
|
||||
try {
|
||||
textarea.setSelectionRange(len, len);
|
||||
} catch {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
applyAutocompletePrefix('/');
|
||||
setCommandQuery('');
|
||||
setAutocompleteTab('commands');
|
||||
setShowCommandAutocomplete(true);
|
||||
setShowFileMention(false);
|
||||
setShowSkillAutocomplete(false);
|
||||
}, [applyAutocompletePrefix, isMobile, setAutocompleteTab, setCommandQuery, setShowCommandAutocomplete, setShowFileMention, setShowSkillAutocomplete]);
|
||||
}, [inputMode, setCommandQuery, setMentionQuery, setShowCommandAutocomplete, setShowFileMention, setShowSkillAutocomplete, setSkillQuery]);
|
||||
|
||||
const insertTextAtSelection = React.useCallback((text: string) => {
|
||||
if (!text) {
|
||||
@@ -4183,9 +4069,6 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
ref={commandRef}
|
||||
searchQuery={commandQuery}
|
||||
onCommandSelect={handleCommandSelect}
|
||||
showTabs={isMobile}
|
||||
activeTab={autocompleteTab}
|
||||
onTabSelect={handleAutocompleteTabSelect}
|
||||
onClose={() => setShowCommandAutocomplete(false)}
|
||||
style={isDesktopExpanded && autocompleteOverlayPosition
|
||||
? {
|
||||
@@ -4245,9 +4128,6 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
searchQuery={mentionQuery}
|
||||
onFileSelect={handleFileSelect}
|
||||
onAgentSelect={handleAgentSelect}
|
||||
showTabs={isMobile}
|
||||
activeTab={autocompleteTab}
|
||||
onTabSelect={handleAutocompleteTabSelect}
|
||||
onClose={() => setShowFileMention(false)}
|
||||
style={isDesktopExpanded && autocompleteOverlayPosition
|
||||
? {
|
||||
@@ -4368,14 +4248,12 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
<div className="flex w-full items-center justify-between gap-x-1.5">
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<ComposerAttachmentControls
|
||||
isMobile={isMobile}
|
||||
isVSCode={isVSCode}
|
||||
footerIconButtonClass={footerIconButtonClass}
|
||||
iconSizeClass={iconSizeClass}
|
||||
fileInputRef={fileInputRef}
|
||||
handleLocalFileSelect={handleLocalFileSelect}
|
||||
handlePickLocalFiles={handlePickLocalFiles}
|
||||
handleOpenCommandMenu={handleOpenCommandMenu}
|
||||
openIssuePicker={openIssuePicker}
|
||||
openPrPicker={openPrPicker}
|
||||
onOpenSettings={onOpenSettings}
|
||||
@@ -4426,14 +4304,12 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
<>
|
||||
<div className={cn("flex items-center flex-shrink-0", footerGapClass)}>
|
||||
<ComposerAttachmentControls
|
||||
isMobile={isMobile}
|
||||
isVSCode={isVSCode}
|
||||
footerIconButtonClass={footerIconButtonClass}
|
||||
iconSizeClass={iconSizeClass}
|
||||
fileInputRef={fileInputRef}
|
||||
handleLocalFileSelect={handleLocalFileSelect}
|
||||
handlePickLocalFiles={handlePickLocalFiles}
|
||||
handleOpenCommandMenu={handleOpenCommandMenu}
|
||||
openIssuePicker={openIssuePicker}
|
||||
openPrPicker={openPrPicker}
|
||||
onOpenSettings={onOpenSettings}
|
||||
|
||||
@@ -27,8 +27,6 @@ export interface CommandAutocompleteHandle {
|
||||
handleKeyDown: (key: string) => void;
|
||||
}
|
||||
|
||||
type AutocompleteTab = 'commands' | 'agents' | 'files';
|
||||
|
||||
const BASE_BADGE_CLASS = "text-[10px] leading-none uppercase font-bold tracking-tight px-1.5 py-1 rounded border flex-shrink-0";
|
||||
const TYPE_BADGE_CLASS = cn(
|
||||
BASE_BADGE_CLASS,
|
||||
@@ -51,9 +49,6 @@ interface CommandAutocompleteProps {
|
||||
searchQuery: string;
|
||||
onCommandSelect: (command: CommandInfo, options?: { dismissKeyboard?: boolean }) => void;
|
||||
onClose: () => void;
|
||||
showTabs?: boolean;
|
||||
activeTab?: AutocompleteTab;
|
||||
onTabSelect?: (tab: AutocompleteTab) => void;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
@@ -61,9 +56,6 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
|
||||
searchQuery,
|
||||
onCommandSelect,
|
||||
onClose,
|
||||
showTabs,
|
||||
activeTab = 'commands',
|
||||
onTabSelect,
|
||||
style,
|
||||
}, ref) => {
|
||||
const { t } = useI18n();
|
||||
@@ -88,7 +80,6 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
|
||||
const ignoreClickRef = React.useRef(false);
|
||||
const pointerStartRef = React.useRef<{ x: number; y: number } | null>(null);
|
||||
const pointerMovedRef = React.useRef(false);
|
||||
const ignoreTabClickRef = React.useRef(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handlePointerDown = (event: MouseEvent | TouchEvent) => {
|
||||
@@ -305,46 +296,6 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
|
||||
className="absolute z-[100] min-w-0 w-full max-w-[450px] max-h-64 bg-background border-2 border-border/60 rounded-xl shadow-none bottom-full mb-2 left-0 flex flex-col"
|
||||
style={style}
|
||||
>
|
||||
{showTabs ? (
|
||||
<div className="px-2 pt-2 pb-1 border-b border-border/60">
|
||||
<div className="flex items-center gap-1 rounded-lg bg-[var(--surface-elevated)] p-1">
|
||||
{([
|
||||
{ id: 'commands' as const, label: t('chat.autocomplete.tabs.commands') },
|
||||
{ id: 'agents' as const, label: t('chat.autocomplete.tabs.agents') },
|
||||
{ id: 'files' as const, label: t('chat.autocomplete.tabs.files') },
|
||||
]).map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
type="button"
|
||||
className={cn(
|
||||
'flex-1 px-2.5 py-1 rounded-md typography-meta font-semibold transition-none',
|
||||
activeTab === tab.id
|
||||
? 'bg-interactive-selection text-interactive-selection-foreground shadow-none'
|
||||
: 'text-muted-foreground hover:bg-interactive-hover/50'
|
||||
)}
|
||||
onPointerDown={(event) => {
|
||||
if (event.pointerType !== 'touch') {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
ignoreTabClickRef.current = true;
|
||||
onTabSelect?.(tab.id);
|
||||
}}
|
||||
onClick={() => {
|
||||
if (ignoreTabClickRef.current) {
|
||||
ignoreTabClickRef.current = false;
|
||||
return;
|
||||
}
|
||||
onTabSelect?.(tab.id);
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<ScrollableOverlay outerClassName="flex-1 min-h-0" className="px-0 pb-2">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
|
||||
@@ -19,23 +19,16 @@ type AgentInfo = {
|
||||
description?: string;
|
||||
mode?: string | null;
|
||||
};
|
||||
const EMPTY_FILES: FileInfo[] = [];
|
||||
const EMPTY_AGENTS: AgentInfo[] = [];
|
||||
|
||||
export interface FileMentionHandle {
|
||||
handleKeyDown: (key: string) => void;
|
||||
}
|
||||
|
||||
type AutocompleteTab = 'commands' | 'agents' | 'files';
|
||||
|
||||
interface FileMentionAutocompleteProps {
|
||||
searchQuery: string;
|
||||
onFileSelect: (file: FileInfo) => void;
|
||||
onAgentSelect?: (agentName: string) => void;
|
||||
onClose: () => void;
|
||||
showTabs?: boolean;
|
||||
activeTab?: AutocompleteTab;
|
||||
onTabSelect?: (tab: AutocompleteTab) => void;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
@@ -44,9 +37,6 @@ export const FileMentionAutocomplete = React.forwardRef<FileMentionHandle, FileM
|
||||
onFileSelect,
|
||||
onAgentSelect,
|
||||
onClose,
|
||||
showTabs,
|
||||
activeTab = 'files',
|
||||
onTabSelect,
|
||||
style,
|
||||
}, ref) => {
|
||||
const { t } = useI18n();
|
||||
@@ -87,9 +77,7 @@ export const FileMentionAutocomplete = React.forwardRef<FileMentionHandle, FileM
|
||||
const labelRefs = React.useRef<(HTMLSpanElement | null)[]>([]);
|
||||
const measureRefs = React.useRef<(HTMLSpanElement | null)[]>([]);
|
||||
const containerRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const ignoreTabClickRef = React.useRef(false);
|
||||
const normalizedSearchQuery = (searchQuery ?? '').trim();
|
||||
const scopeResultsToActiveTab = showTabs === true;
|
||||
const recentFiles = React.useMemo(() => {
|
||||
if (!projectRoot || !projectTabs) {
|
||||
return [] as FileInfo[];
|
||||
@@ -128,14 +116,12 @@ export const FileMentionAutocomplete = React.forwardRef<FileMentionHandle, FileM
|
||||
return mapped;
|
||||
}, [normalizedSearchQuery, projectRoot, projectTabs]);
|
||||
const visibleAgents = React.useMemo(
|
||||
() => !scopeResultsToActiveTab || activeTab === 'agents'
|
||||
? (normalizedSearchQuery.length > 0 ? agents : agents.slice(0, 2))
|
||||
: EMPTY_AGENTS,
|
||||
[activeTab, agents, normalizedSearchQuery.length, scopeResultsToActiveTab],
|
||||
() => normalizedSearchQuery.length > 0 ? agents : agents.slice(0, 2),
|
||||
[agents, normalizedSearchQuery.length],
|
||||
);
|
||||
const visibleDirectories = !scopeResultsToActiveTab || activeTab === 'files' ? directories : EMPTY_FILES;
|
||||
const visibleRecentFiles = !scopeResultsToActiveTab || activeTab === 'files' ? recentFiles : EMPTY_FILES;
|
||||
const visibleFiles = !scopeResultsToActiveTab || activeTab === 'files' ? files : EMPTY_FILES;
|
||||
const visibleDirectories = directories;
|
||||
const visibleRecentFiles = recentFiles;
|
||||
const visibleFiles = files;
|
||||
|
||||
React.useEffect(() => {
|
||||
const handlePointerDown = (event: MouseEvent | TouchEvent) => {
|
||||
@@ -452,56 +438,14 @@ export const FileMentionAutocomplete = React.forwardRef<FileMentionHandle, FileM
|
||||
}
|
||||
};
|
||||
|
||||
const tabs = React.useMemo(() => ([
|
||||
{ id: 'commands' as const, label: t('chat.autocomplete.tabs.commands') },
|
||||
{ id: 'agents' as const, label: t('chat.autocomplete.tabs.agents') },
|
||||
{ id: 'files' as const, label: t('chat.autocomplete.tabs.files') },
|
||||
]), [t]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="absolute z-[100] min-w-0 w-full max-w-[640px] max-h-64 bg-background border-2 border-border/60 rounded-xl shadow-none bottom-full mb-2 left-0 flex flex-col"
|
||||
style={style}
|
||||
>
|
||||
{showTabs ? (
|
||||
<div className="px-2 pt-2 pb-1 border-b border-border/60">
|
||||
<div className="flex items-center gap-1 rounded-lg bg-[var(--surface-elevated)] p-1">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
type="button"
|
||||
className={cn(
|
||||
'flex-1 px-2.5 py-1 rounded-md typography-meta font-semibold transition-none',
|
||||
activeTab === tab.id
|
||||
? 'bg-interactive-selection text-interactive-selection-foreground shadow-none'
|
||||
: 'text-muted-foreground hover:bg-interactive-hover/50'
|
||||
)}
|
||||
onPointerDown={(event) => {
|
||||
if (event.pointerType !== 'touch') {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
ignoreTabClickRef.current = true;
|
||||
onTabSelect?.(tab.id);
|
||||
}}
|
||||
onClick={() => {
|
||||
if (ignoreTabClickRef.current) {
|
||||
ignoreTabClickRef.current = false;
|
||||
return;
|
||||
}
|
||||
onTabSelect?.(tab.id);
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<ScrollableOverlay outerClassName="flex-1 min-h-0" className="px-0">
|
||||
{(!scopeResultsToActiveTab || activeTab === 'files') && loading ? (
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<Icon name="refresh" className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,6 @@ import { useUIStore } from '@/stores/useUIStore';
|
||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
import { useUpdateStore } from '@/stores/useUpdateStore';
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
import { useVisualViewport } from '@/hooks/useVisualViewport';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery';
|
||||
|
||||
@@ -60,7 +59,6 @@ export const MainLayout: React.FC = () => {
|
||||
const setMultiRunLauncherOpen = useUIStore((state) => state.setMultiRunLauncherOpen);
|
||||
const multiRunLauncherPrefillPrompt = useUIStore((state) => state.multiRunLauncherPrefillPrompt);
|
||||
const { isMobile, isTablet } = useDeviceInfo();
|
||||
const visualViewport = useVisualViewport();
|
||||
const sidebarWidth = useUIStore((state) => state.sidebarWidth);
|
||||
const rightSidebarWidth = useUIStore((state) => state.rightSidebarWidth);
|
||||
const rightSidebarAutoClosedRef = React.useRef(false);
|
||||
@@ -431,10 +429,9 @@ export const MainLayout: React.FC = () => {
|
||||
data-page-scroll-lock="true"
|
||||
className={cn(
|
||||
'main-content-safe-area',
|
||||
isMobile ? 'flex flex-col' : 'flex h-[100dvh]',
|
||||
isMobile ? 'flex h-[100dvh] flex-col' : 'flex h-[100dvh]',
|
||||
'bg-background'
|
||||
)}
|
||||
style={isMobile && visualViewport.height > 0 ? { height: visualViewport.height } : undefined}
|
||||
>
|
||||
<CommandPalette />
|
||||
<HelpDialog />
|
||||
|
||||
@@ -5,24 +5,7 @@ export const VIEWPORT_META_SELECTOR = 'meta[name="viewport"]';
|
||||
export const VIEWPORT_CONTENT_BASE = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover';
|
||||
|
||||
export const supportsMobileKeyboardResizeContent = (): boolean => {
|
||||
if (typeof navigator === 'undefined') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const userAgent = navigator.userAgent || '';
|
||||
const platform = navigator.platform || '';
|
||||
const maxTouchPoints = navigator.maxTouchPoints ?? 0;
|
||||
const isIOS = /iPhone|iPad|iPod/i.test(userAgent)
|
||||
|| ((/Macintosh|MacIntel/i.test(userAgent) || /MacIntel/i.test(platform)) && maxTouchPoints > 1);
|
||||
|
||||
return !isIOS;
|
||||
};
|
||||
|
||||
const getSupportedMobileKeyboardMode = (mode: MobileKeyboardMode): MobileKeyboardMode => {
|
||||
if (mode === 'resize-content' && !supportsMobileKeyboardResizeContent()) {
|
||||
return 'native';
|
||||
}
|
||||
return mode;
|
||||
return true;
|
||||
};
|
||||
|
||||
export function normalizeMobileKeyboardMode(value: unknown): MobileKeyboardMode;
|
||||
@@ -30,7 +13,7 @@ export function normalizeMobileKeyboardMode(value: unknown, fallback: MobileKeyb
|
||||
export function normalizeMobileKeyboardMode(value: unknown, fallback: undefined): MobileKeyboardMode | undefined;
|
||||
export function normalizeMobileKeyboardMode(
|
||||
value: unknown,
|
||||
fallback: MobileKeyboardMode | undefined = 'native',
|
||||
fallback: MobileKeyboardMode | undefined = 'resize-content',
|
||||
): MobileKeyboardMode | undefined {
|
||||
if (value === 'native' || value === 'resize-content') {
|
||||
return value;
|
||||
@@ -39,7 +22,7 @@ export function normalizeMobileKeyboardMode(
|
||||
}
|
||||
|
||||
export const getViewportContentForMobileKeyboardMode = (value: unknown): string => {
|
||||
const mode = getSupportedMobileKeyboardMode(normalizeMobileKeyboardMode(value));
|
||||
const mode = normalizeMobileKeyboardMode(value);
|
||||
return mode === 'resize-content'
|
||||
? `${VIEWPORT_CONTENT_BASE}, interactive-widget=resizes-content`
|
||||
: VIEWPORT_CONTENT_BASE;
|
||||
@@ -47,22 +30,22 @@ export const getViewportContentForMobileKeyboardMode = (value: unknown): string
|
||||
|
||||
export const getStoredMobileKeyboardMode = (): MobileKeyboardMode => {
|
||||
if (typeof window === 'undefined') {
|
||||
return 'native';
|
||||
return 'resize-content';
|
||||
}
|
||||
|
||||
try {
|
||||
return getSupportedMobileKeyboardMode(normalizeMobileKeyboardMode(localStorage.getItem(MOBILE_KEYBOARD_MODE_STORAGE_KEY)));
|
||||
return normalizeMobileKeyboardMode(localStorage.getItem(MOBILE_KEYBOARD_MODE_STORAGE_KEY));
|
||||
} catch {
|
||||
return 'native';
|
||||
return 'resize-content';
|
||||
}
|
||||
};
|
||||
|
||||
export const setStoredMobileKeyboardMode = (value: unknown): MobileKeyboardMode => {
|
||||
const mode = getSupportedMobileKeyboardMode(normalizeMobileKeyboardMode(value));
|
||||
const mode = normalizeMobileKeyboardMode(value);
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
if (mode === 'native') {
|
||||
if (mode === 'resize-content') {
|
||||
localStorage.removeItem(MOBILE_KEYBOARD_MODE_STORAGE_KEY);
|
||||
} else {
|
||||
localStorage.setItem(MOBILE_KEYBOARD_MODE_STORAGE_KEY, mode);
|
||||
|
||||
Reference in New Issue
Block a user