fix(ui): add opt-in mobile keyboard resize mode and stabilize touch terminal input (#1107)
* fix(ui): add opt-in mobile keyboard resize mode * fix(ui): stabilize touch terminal input and tab layout * fix(ui): narrow touch terminal handling to mobile and tablet * fix(ui): refine touch terminal input handling * fix(ui): re-key terminal viewport on session id * fix(ui): avoid touch-laptop terminal overlay regression * fix(ui): hide ghostty system caret surfaces * fix(settings): normalize mobile keyboard mode sanitization * fix(ui): honor terminal quick keys toggle on mobile * fix(ui): gate mobile keyboard resize mode on iOS --------- Co-authored-by: vhqtvn <8930337+vhqtvn@users.noreply.github.com> Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
vhqtvn
Bohdan Triapitsyn
parent
63b4a5b996
commit
962ee41bba
@@ -594,6 +594,7 @@ export interface SettingsPayload {
|
||||
gitProviderId?: string;
|
||||
gitModelId?: string;
|
||||
pwaAppName?: string;
|
||||
mobileKeyboardMode?: 'native' | 'resize-content';
|
||||
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useUIStore } from '@/stores/useUIStore';
|
||||
import { updateDesktopSettings } from '@/lib/persistence';
|
||||
import type { DesktopSettings } from '@/lib/desktop';
|
||||
import type { MonoFontOption, UiFontOption } from '@/lib/fontOptions';
|
||||
import type { MobileKeyboardMode } from '@/lib/mobileKeyboardMode';
|
||||
|
||||
type AppearanceSlice = {
|
||||
showReasoningTraces: boolean;
|
||||
@@ -32,6 +33,7 @@ type AppearanceSlice = {
|
||||
padding: number;
|
||||
cornerRadius: number;
|
||||
inputBarOffset: number;
|
||||
mobileKeyboardMode: MobileKeyboardMode;
|
||||
diffLayoutPreference: 'dynamic' | 'inline' | 'side-by-side';
|
||||
diffViewMode: 'single' | 'stacked';
|
||||
gitChangesViewMode: 'flat' | 'tree';
|
||||
@@ -70,6 +72,7 @@ export const startAppearanceAutoSave = (): void => {
|
||||
padding: useUIStore.getState().padding,
|
||||
cornerRadius: useUIStore.getState().cornerRadius,
|
||||
inputBarOffset: useUIStore.getState().inputBarOffset,
|
||||
mobileKeyboardMode: useUIStore.getState().mobileKeyboardMode,
|
||||
diffLayoutPreference: useUIStore.getState().diffLayoutPreference,
|
||||
diffViewMode: useUIStore.getState().diffViewMode,
|
||||
gitChangesViewMode: useUIStore.getState().gitChangesViewMode,
|
||||
@@ -120,6 +123,7 @@ export const startAppearanceAutoSave = (): void => {
|
||||
padding: state.padding,
|
||||
cornerRadius: state.cornerRadius,
|
||||
inputBarOffset: state.inputBarOffset,
|
||||
mobileKeyboardMode: state.mobileKeyboardMode,
|
||||
diffLayoutPreference: state.diffLayoutPreference,
|
||||
diffViewMode: state.diffViewMode,
|
||||
gitChangesViewMode: state.gitChangesViewMode,
|
||||
@@ -196,6 +200,9 @@ export const startAppearanceAutoSave = (): void => {
|
||||
if (current.inputBarOffset !== previous.inputBarOffset) {
|
||||
diff.inputBarOffset = current.inputBarOffset;
|
||||
}
|
||||
if (current.mobileKeyboardMode !== previous.mobileKeyboardMode) {
|
||||
diff.mobileKeyboardMode = current.mobileKeyboardMode;
|
||||
}
|
||||
if (current.diffLayoutPreference !== previous.diffLayoutPreference) {
|
||||
diff.diffLayoutPreference = current.diffLayoutPreference;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ProjectEntry } from '@/lib/api/types';
|
||||
import type { MobileKeyboardMode } from '@/lib/mobileKeyboardMode';
|
||||
|
||||
export type AssistantNotificationPayload = {
|
||||
title?: string;
|
||||
@@ -121,6 +122,7 @@ export type DesktopSettings = {
|
||||
gitModelId?: string;
|
||||
pwaAppName?: string;
|
||||
pwaOrientation?: 'system' | 'portrait' | 'landscape';
|
||||
mobileKeyboardMode?: MobileKeyboardMode;
|
||||
inputSpellcheckEnabled?: boolean;
|
||||
showToolFileIcons?: boolean;
|
||||
showExpandedBashTools?: boolean;
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface DeviceInfo {
|
||||
screenWidth: number;
|
||||
breakpoint: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
|
||||
hasTouchInput: boolean;
|
||||
hasTouchOnlyPointer: boolean;
|
||||
}
|
||||
|
||||
export const CSS_DEVICE_VARIABLES = {
|
||||
@@ -100,6 +101,7 @@ export function getDeviceInfo(): DeviceInfo {
|
||||
const { isExplicitTablet } = getNavigatorDeviceHints(maxTouchPoints);
|
||||
|
||||
const hasTouchInput = prefersCoarsePointer || noHover || maxTouchPoints > 0;
|
||||
const hasTouchOnlyPointer = prefersCoarsePointer || noHover;
|
||||
|
||||
const isTabletWidth = width > BREAKPOINTS.md && width <= BREAKPOINTS.lg;
|
||||
const isMobileWidth = width <= BREAKPOINTS.md;
|
||||
@@ -140,6 +142,7 @@ export function getDeviceInfo(): DeviceInfo {
|
||||
screenWidth: width,
|
||||
breakpoint,
|
||||
hasTouchInput,
|
||||
hasTouchOnlyPointer,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -231,6 +234,7 @@ export function useDeviceInfo(): DeviceInfo {
|
||||
screenWidth: 1024,
|
||||
breakpoint: 'lg',
|
||||
hasTouchInput: false,
|
||||
hasTouchOnlyPointer: false,
|
||||
};
|
||||
}
|
||||
return getDeviceInfo();
|
||||
|
||||
@@ -1384,6 +1384,11 @@ export const settingsDict = {
|
||||
'settings.openchamber.visual.field.pwaInstallOrientationAria': 'PWA install orientation',
|
||||
'settings.openchamber.visual.field.selectOrientationPlaceholder': 'Select orientation',
|
||||
'settings.openchamber.visual.actions.resetInstallOrientationAria': 'Reset install orientation',
|
||||
'settings.openchamber.visual.field.mobileKeyboardMode': 'Mobile Keyboard Behavior',
|
||||
'settings.openchamber.visual.field.mobileKeyboardModeHint': 'Default browser behavior is safest. Resize content asks supported browsers to shrink the app when the on-screen keyboard opens.',
|
||||
'settings.openchamber.visual.field.mobileKeyboardModeAria': 'Mobile keyboard behavior',
|
||||
'settings.openchamber.visual.field.selectMobileKeyboardModePlaceholder': 'Select keyboard behavior',
|
||||
'settings.openchamber.visual.actions.resetMobileKeyboardModeAria': 'Reset mobile keyboard behavior',
|
||||
'settings.openchamber.visual.field.interfaceFontSize': 'Interface Font Size',
|
||||
'settings.openchamber.visual.field.interfaceFont': 'Interface Font',
|
||||
'settings.openchamber.visual.field.selectInterfaceFontAria': 'Select interface font',
|
||||
@@ -1460,6 +1465,10 @@ export const settingsDict = {
|
||||
'settings.openchamber.visual.option.pwaOrientation.portrait.description': 'Install the app locked to portrait.',
|
||||
'settings.openchamber.visual.option.pwaOrientation.landscape.label': 'Landscape lock',
|
||||
'settings.openchamber.visual.option.pwaOrientation.landscape.description': 'Install the app locked to landscape.',
|
||||
'settings.openchamber.visual.option.mobileKeyboardMode.native.label': 'Follow browser',
|
||||
'settings.openchamber.visual.option.mobileKeyboardMode.native.description': 'Use the browser default for panning and viewport changes when the keyboard opens.',
|
||||
'settings.openchamber.visual.option.mobileKeyboardMode.resizeContent.label': 'Resize content',
|
||||
'settings.openchamber.visual.option.mobileKeyboardMode.resizeContent.description': 'Ask supported browsers to shrink the app layout instead of relying only on panning.',
|
||||
'settings.openchamber.visual.option.userMessageRendering.markdown.label': 'Markdown',
|
||||
'settings.openchamber.visual.option.userMessageRendering.markdown.description': 'Render user text with markdown formatting.',
|
||||
'settings.openchamber.visual.option.userMessageRendering.plain.label': 'Plain text',
|
||||
|
||||
@@ -1384,6 +1384,11 @@ export const settingsDict = {
|
||||
"settings.openchamber.visual.field.pwaInstallOrientationAria": "Orientación de instalación de PWA",
|
||||
"settings.openchamber.visual.field.selectOrientationPlaceholder": "Seleccionar orientación",
|
||||
"settings.openchamber.visual.actions.resetInstallOrientationAria": "Restablecer orientación de instalación",
|
||||
"settings.openchamber.visual.field.mobileKeyboardMode": "Comportamiento del teclado móvil",
|
||||
"settings.openchamber.visual.field.mobileKeyboardModeHint": "El comportamiento predeterminado del navegador es el más seguro. Redimensionar el contenido pide a los navegadores compatibles reducir la app cuando se abre el teclado.",
|
||||
"settings.openchamber.visual.field.mobileKeyboardModeAria": "Comportamiento del teclado móvil",
|
||||
"settings.openchamber.visual.field.selectMobileKeyboardModePlaceholder": "Seleccionar comportamiento del teclado",
|
||||
"settings.openchamber.visual.actions.resetMobileKeyboardModeAria": "Restablecer comportamiento del teclado móvil",
|
||||
"settings.openchamber.visual.field.interfaceFontSize": "Tamaño de fuente de la interfaz",
|
||||
"settings.openchamber.visual.field.interfaceFont": "Fuente de la interfaz",
|
||||
"settings.openchamber.visual.field.selectInterfaceFontAria": "Seleccionar fuente de interfaz",
|
||||
@@ -1460,6 +1465,10 @@ export const settingsDict = {
|
||||
"settings.openchamber.visual.option.pwaOrientation.portrait.description": "Instalar la aplicación bloqueada en vertical.",
|
||||
"settings.openchamber.visual.option.pwaOrientation.landscape.label": "Bloqueo horizontal",
|
||||
"settings.openchamber.visual.option.pwaOrientation.landscape.description": "Instalar la aplicación bloqueada en horizontal.",
|
||||
"settings.openchamber.visual.option.mobileKeyboardMode.native.label": "Seguir el navegador",
|
||||
"settings.openchamber.visual.option.mobileKeyboardMode.native.description": "Usar el comportamiento predeterminado del navegador para el paneo y los cambios del viewport al abrir el teclado.",
|
||||
"settings.openchamber.visual.option.mobileKeyboardMode.resizeContent.label": "Redimensionar contenido",
|
||||
"settings.openchamber.visual.option.mobileKeyboardMode.resizeContent.description": "Pedir a los navegadores compatibles que reduzcan el diseño de la app en lugar de depender solo del paneo.",
|
||||
"settings.openchamber.visual.option.userMessageRendering.markdown.label": "Markdown",
|
||||
"settings.openchamber.visual.option.userMessageRendering.markdown.description": "Renderiza el texto del usuario con formato markdown.",
|
||||
"settings.openchamber.visual.option.userMessageRendering.plain.label": "Texto plano",
|
||||
|
||||
@@ -1384,6 +1384,11 @@ export const settingsDict = {
|
||||
'settings.openchamber.visual.field.pwaInstallOrientationAria': 'PWA 설치 방향',
|
||||
'settings.openchamber.visual.field.selectOrientationPlaceholder': '방향 선택',
|
||||
'settings.openchamber.visual.actions.resetInstallOrientationAria': '설치 방향 초기화',
|
||||
'settings.openchamber.visual.field.mobileKeyboardMode': '모바일 키보드 동작',
|
||||
'settings.openchamber.visual.field.mobileKeyboardModeHint': '기본 브라우저 동작이 가장 안전합니다. 콘텐츠 크기 조정은 지원되는 브라우저에 키보드가 열릴 때 앱을 줄이도록 요청합니다.',
|
||||
'settings.openchamber.visual.field.mobileKeyboardModeAria': '모바일 키보드 동작',
|
||||
'settings.openchamber.visual.field.selectMobileKeyboardModePlaceholder': '키보드 동작 선택',
|
||||
'settings.openchamber.visual.actions.resetMobileKeyboardModeAria': '모바일 키보드 동작 초기화',
|
||||
'settings.openchamber.visual.field.interfaceFontSize': '인터페이스 폰트 크기',
|
||||
'settings.openchamber.visual.field.interfaceFont': '인터페이스 폰트',
|
||||
'settings.openchamber.visual.field.selectInterfaceFontAria': '인터페이스 폰트 선택',
|
||||
@@ -1460,6 +1465,10 @@ export const settingsDict = {
|
||||
'settings.openchamber.visual.option.pwaOrientation.portrait.description': '앱을 세로로 고정해 설치합니다.',
|
||||
'settings.openchamber.visual.option.pwaOrientation.landscape.label': '가로 고정',
|
||||
'settings.openchamber.visual.option.pwaOrientation.landscape.description': '앱을 가로로 고정해 설치합니다.',
|
||||
'settings.openchamber.visual.option.mobileKeyboardMode.native.label': '브라우저 따르기',
|
||||
'settings.openchamber.visual.option.mobileKeyboardMode.native.description': '키보드가 열릴 때 브라우저 기본 패닝 및 뷰포트 동작을 사용합니다.',
|
||||
'settings.openchamber.visual.option.mobileKeyboardMode.resizeContent.label': '콘텐츠 크기 조정',
|
||||
'settings.openchamber.visual.option.mobileKeyboardMode.resizeContent.description': '지원되는 브라우저에 패닝만 의존하지 않고 앱 레이아웃을 줄이도록 요청합니다.',
|
||||
'settings.openchamber.visual.option.userMessageRendering.markdown.label': 'Markdown',
|
||||
'settings.openchamber.visual.option.userMessageRendering.markdown.description': '사용자 텍스트를 Markdown 서식으로 렌더링합니다.',
|
||||
'settings.openchamber.visual.option.userMessageRendering.plain.label': 'Plain text',
|
||||
|
||||
@@ -1384,6 +1384,11 @@ export const settingsDict = {
|
||||
"settings.openchamber.visual.field.pwaInstallOrientationAria": "Orientação de instalação da PWA",
|
||||
"settings.openchamber.visual.field.selectOrientationPlaceholder": "Selecionar orientação",
|
||||
"settings.openchamber.visual.actions.resetInstallOrientationAria": "Redefinir orientação de instalação",
|
||||
"settings.openchamber.visual.field.mobileKeyboardMode": "Comportamento do teclado móvel",
|
||||
"settings.openchamber.visual.field.mobileKeyboardModeHint": "O comportamento padrão do navegador é o mais seguro. Redimensionar conteúdo pede aos navegadores compatíveis que encolham o app quando o teclado abre.",
|
||||
"settings.openchamber.visual.field.mobileKeyboardModeAria": "Comportamento do teclado móvel",
|
||||
"settings.openchamber.visual.field.selectMobileKeyboardModePlaceholder": "Selecionar comportamento do teclado",
|
||||
"settings.openchamber.visual.actions.resetMobileKeyboardModeAria": "Redefinir comportamento do teclado móvel",
|
||||
"settings.openchamber.visual.field.interfaceFontSize": "Tamanho base da interface",
|
||||
"settings.openchamber.visual.field.interfaceFont": "Fonte da interface",
|
||||
"settings.openchamber.visual.field.selectInterfaceFontAria": "Selecionar fonte da interface",
|
||||
@@ -1460,6 +1465,10 @@ export const settingsDict = {
|
||||
"settings.openchamber.visual.option.pwaOrientation.portrait.description": "Instalar o aplicativo bloqueado na vertical.",
|
||||
"settings.openchamber.visual.option.pwaOrientation.landscape.label": "Bloqueio horizontal",
|
||||
"settings.openchamber.visual.option.pwaOrientation.landscape.description": "Instalar o aplicativo bloqueado na horizontal.",
|
||||
"settings.openchamber.visual.option.mobileKeyboardMode.native.label": "Seguir navegador",
|
||||
"settings.openchamber.visual.option.mobileKeyboardMode.native.description": "Usar o padrão do navegador para pan e mudanças de viewport quando o teclado abrir.",
|
||||
"settings.openchamber.visual.option.mobileKeyboardMode.resizeContent.label": "Redimensionar conteúdo",
|
||||
"settings.openchamber.visual.option.mobileKeyboardMode.resizeContent.description": "Pedir aos navegadores compatíveis que encolham o layout do app em vez de depender só do pan.",
|
||||
"settings.openchamber.visual.option.userMessageRendering.markdown.label": "Markdown",
|
||||
"settings.openchamber.visual.option.userMessageRendering.markdown.description": "Renderiza o texto do usuário com formato markdown.",
|
||||
"settings.openchamber.visual.option.userMessageRendering.plain.label": "Texto simples",
|
||||
|
||||
@@ -1384,6 +1384,11 @@ export const settingsDict = {
|
||||
"settings.openchamber.visual.field.pwaInstallOrientationAria": "Орієнтація встановлення PWA",
|
||||
"settings.openchamber.visual.field.selectOrientationPlaceholder": "Вибрати орієнтацію",
|
||||
"settings.openchamber.visual.actions.resetInstallOrientationAria": "Скинути орієнтацію встановлення",
|
||||
"settings.openchamber.visual.field.mobileKeyboardMode": "Поведінка мобільної клавіатури",
|
||||
"settings.openchamber.visual.field.mobileKeyboardModeHint": "Типова поведінка браузера є найбезпечнішою. Режим зміни розміру просить сумісні браузери зменшувати застосунок, коли відкривається клавіатура.",
|
||||
"settings.openchamber.visual.field.mobileKeyboardModeAria": "Поведінка мобільної клавіатури",
|
||||
"settings.openchamber.visual.field.selectMobileKeyboardModePlaceholder": "Вибрати поведінку клавіатури",
|
||||
"settings.openchamber.visual.actions.resetMobileKeyboardModeAria": "Скинути поведінку мобільної клавіатури",
|
||||
"settings.openchamber.visual.field.interfaceFontSize": "Розмір шрифту інтерфейсу",
|
||||
"settings.openchamber.visual.field.interfaceFont": "Шрифт інтерфейсу",
|
||||
"settings.openchamber.visual.field.selectInterfaceFontAria": "Вибрати шрифт інтерфейсу",
|
||||
@@ -1460,6 +1465,10 @@ export const settingsDict = {
|
||||
"settings.openchamber.visual.option.pwaOrientation.portrait.description": "Встановлювати застосунок у портретній орієнтації.",
|
||||
"settings.openchamber.visual.option.pwaOrientation.landscape.label": "Зафіксувати альбомну",
|
||||
"settings.openchamber.visual.option.pwaOrientation.landscape.description": "Встановлювати застосунок в альбомній орієнтації.",
|
||||
"settings.openchamber.visual.option.mobileKeyboardMode.native.label": "За браузером",
|
||||
"settings.openchamber.visual.option.mobileKeyboardMode.native.description": "Використовувати типову поведінку браузера для панорамування та змін viewport, коли відкривається клавіатура.",
|
||||
"settings.openchamber.visual.option.mobileKeyboardMode.resizeContent.label": "Змінювати розмір вмісту",
|
||||
"settings.openchamber.visual.option.mobileKeyboardMode.resizeContent.description": "Просити сумісні браузери зменшувати макет застосунку замість покладатися лише на панорамування.",
|
||||
"settings.openchamber.visual.option.userMessageRendering.markdown.label": "Markdown",
|
||||
"settings.openchamber.visual.option.userMessageRendering.markdown.description": "Виводити текст користувача з форматуванням розмітки.",
|
||||
"settings.openchamber.visual.option.userMessageRendering.plain.label": "Звичайний текст",
|
||||
|
||||
@@ -1384,6 +1384,11 @@ export const settingsDict = {
|
||||
'settings.openchamber.visual.field.pwaInstallOrientationAria': 'PWA 安装方向',
|
||||
'settings.openchamber.visual.field.selectOrientationPlaceholder': '选择方向',
|
||||
'settings.openchamber.visual.actions.resetInstallOrientationAria': '重置安装方向',
|
||||
'settings.openchamber.visual.field.mobileKeyboardMode': '移动键盘行为',
|
||||
'settings.openchamber.visual.field.mobileKeyboardModeHint': '默认浏览器行为最稳妥。调整内容大小会请求受支持的浏览器在键盘打开时缩小应用布局。',
|
||||
'settings.openchamber.visual.field.mobileKeyboardModeAria': '移动键盘行为',
|
||||
'settings.openchamber.visual.field.selectMobileKeyboardModePlaceholder': '选择键盘行为',
|
||||
'settings.openchamber.visual.actions.resetMobileKeyboardModeAria': '重置移动键盘行为',
|
||||
'settings.openchamber.visual.field.interfaceFontSize': '界面字体大小',
|
||||
'settings.openchamber.visual.field.interfaceFont': '界面字体',
|
||||
'settings.openchamber.visual.field.selectInterfaceFontAria': '选择界面字体',
|
||||
@@ -1460,6 +1465,10 @@ export const settingsDict = {
|
||||
'settings.openchamber.visual.option.pwaOrientation.portrait.description': '安装后应用锁定为竖屏。',
|
||||
'settings.openchamber.visual.option.pwaOrientation.landscape.label': '锁定横屏',
|
||||
'settings.openchamber.visual.option.pwaOrientation.landscape.description': '安装后应用锁定为横屏。',
|
||||
'settings.openchamber.visual.option.mobileKeyboardMode.native.label': '跟随浏览器',
|
||||
'settings.openchamber.visual.option.mobileKeyboardMode.native.description': '键盘打开时使用浏览器默认的平移和视口变化行为。',
|
||||
'settings.openchamber.visual.option.mobileKeyboardMode.resizeContent.label': '调整内容大小',
|
||||
'settings.openchamber.visual.option.mobileKeyboardMode.resizeContent.description': '请求受支持的浏览器缩小应用布局,而不是只依赖平移。',
|
||||
'settings.openchamber.visual.option.userMessageRendering.markdown.label': 'Markdown',
|
||||
'settings.openchamber.visual.option.userMessageRendering.markdown.description': '用 Markdown 格式渲染用户文本。',
|
||||
'settings.openchamber.visual.option.userMessageRendering.plain.label': '纯文本',
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
export type MobileKeyboardMode = 'native' | 'resize-content';
|
||||
|
||||
export const MOBILE_KEYBOARD_MODE_STORAGE_KEY = 'openchamber.mobileKeyboardMode';
|
||||
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;
|
||||
};
|
||||
|
||||
export function normalizeMobileKeyboardMode(value: unknown): MobileKeyboardMode;
|
||||
export function normalizeMobileKeyboardMode(value: unknown, fallback: MobileKeyboardMode): MobileKeyboardMode;
|
||||
export function normalizeMobileKeyboardMode(value: unknown, fallback: undefined): MobileKeyboardMode | undefined;
|
||||
export function normalizeMobileKeyboardMode(
|
||||
value: unknown,
|
||||
fallback: MobileKeyboardMode | undefined = 'native',
|
||||
): MobileKeyboardMode | undefined {
|
||||
if (value === 'native' || value === 'resize-content') {
|
||||
return value;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export const getViewportContentForMobileKeyboardMode = (value: unknown): string => {
|
||||
const mode = getSupportedMobileKeyboardMode(normalizeMobileKeyboardMode(value));
|
||||
return mode === 'resize-content'
|
||||
? `${VIEWPORT_CONTENT_BASE}, interactive-widget=resizes-content`
|
||||
: VIEWPORT_CONTENT_BASE;
|
||||
};
|
||||
|
||||
export const getStoredMobileKeyboardMode = (): MobileKeyboardMode => {
|
||||
if (typeof window === 'undefined') {
|
||||
return 'native';
|
||||
}
|
||||
|
||||
try {
|
||||
return getSupportedMobileKeyboardMode(normalizeMobileKeyboardMode(localStorage.getItem(MOBILE_KEYBOARD_MODE_STORAGE_KEY)));
|
||||
} catch {
|
||||
return 'native';
|
||||
}
|
||||
};
|
||||
|
||||
export const setStoredMobileKeyboardMode = (value: unknown): MobileKeyboardMode => {
|
||||
const mode = getSupportedMobileKeyboardMode(normalizeMobileKeyboardMode(value));
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
if (mode === 'native') {
|
||||
localStorage.removeItem(MOBILE_KEYBOARD_MODE_STORAGE_KEY);
|
||||
} else {
|
||||
localStorage.setItem(MOBILE_KEYBOARD_MODE_STORAGE_KEY, mode);
|
||||
}
|
||||
} catch {
|
||||
// Ignore storage failures in restricted browsing contexts.
|
||||
}
|
||||
}
|
||||
|
||||
return mode;
|
||||
};
|
||||
|
||||
export const applyMobileKeyboardMode = (value: unknown): MobileKeyboardMode => {
|
||||
const mode = setStoredMobileKeyboardMode(value);
|
||||
|
||||
if (typeof document === 'undefined') {
|
||||
return mode;
|
||||
}
|
||||
|
||||
document.documentElement.setAttribute('data-oc-mobile-keyboard-mode', mode);
|
||||
|
||||
const viewportMeta = document.querySelector(VIEWPORT_META_SELECTOR);
|
||||
if (viewportMeta instanceof HTMLMetaElement) {
|
||||
const nextContent = getViewportContentForMobileKeyboardMode(mode);
|
||||
if (viewportMeta.getAttribute('content') !== nextContent) {
|
||||
viewportMeta.setAttribute('content', nextContent);
|
||||
}
|
||||
}
|
||||
|
||||
return mode;
|
||||
};
|
||||
@@ -7,6 +7,7 @@ import { setDirectoryShowHidden } from '@/lib/directoryShowHidden';
|
||||
import { setFilesViewShowGitignored } from '@/lib/filesViewShowGitignored';
|
||||
import { loadAppearancePreferences, applyAppearancePreferences } from '@/lib/appearancePersistence';
|
||||
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
||||
import { normalizeMobileKeyboardMode, setStoredMobileKeyboardMode } from '@/lib/mobileKeyboardMode';
|
||||
|
||||
const persistToLocalStorage = (settings: DesktopSettings) => {
|
||||
if (typeof window === 'undefined') {
|
||||
@@ -92,6 +93,9 @@ const persistToLocalStorage = (settings: DesktopSettings) => {
|
||||
localStorage.removeItem('openchamber.pwaName');
|
||||
}
|
||||
}
|
||||
if (typeof settings.mobileKeyboardMode === 'string') {
|
||||
setStoredMobileKeyboardMode(settings.mobileKeyboardMode);
|
||||
}
|
||||
};
|
||||
|
||||
type PersistApi = {
|
||||
@@ -464,6 +468,12 @@ const applyDesktopUiPreferences = (settings: DesktopSettings) => {
|
||||
if (typeof settings.inputBarOffset === 'number' && Number.isFinite(settings.inputBarOffset) && settings.inputBarOffset !== store.inputBarOffset) {
|
||||
store.setInputBarOffset(settings.inputBarOffset);
|
||||
}
|
||||
if (typeof settings.mobileKeyboardMode === 'string') {
|
||||
const mode = normalizeMobileKeyboardMode(settings.mobileKeyboardMode, store.mobileKeyboardMode);
|
||||
if (mode !== store.mobileKeyboardMode) {
|
||||
store.setMobileKeyboardMode(mode);
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(settings.favoriteModels)) {
|
||||
const current = store.favoriteModels;
|
||||
@@ -889,6 +899,12 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => {
|
||||
if (typeof candidate.inputBarOffset === 'number' && Number.isFinite(candidate.inputBarOffset)) {
|
||||
result.inputBarOffset = candidate.inputBarOffset;
|
||||
}
|
||||
if (typeof candidate.mobileKeyboardMode === 'string') {
|
||||
const mode = normalizeMobileKeyboardMode(candidate.mobileKeyboardMode, undefined);
|
||||
if (mode) {
|
||||
result.mobileKeyboardMode = mode;
|
||||
}
|
||||
}
|
||||
|
||||
const favoriteModels = sanitizeModelRefs(candidate.favoriteModels, 64);
|
||||
if (favoriteModels) {
|
||||
|
||||
@@ -152,7 +152,7 @@ export const SETTINGS_PAGE_METADATA: readonly SettingsPageMeta[] = [
|
||||
title: 'Appearance',
|
||||
group: 'appearance',
|
||||
kind: 'single',
|
||||
keywords: ['theme', 'font', 'spacing', 'padding', 'corner radius', 'radius', 'input bar', 'terminal', 'pwa', 'install name', 'app shortcuts'],
|
||||
keywords: ['theme', 'font', 'spacing', 'padding', 'corner radius', 'radius', 'input bar', 'keyboard', 'viewport', 'mobile', 'terminal', 'pwa', 'install name', 'app shortcuts'],
|
||||
},
|
||||
{
|
||||
slug: 'chat',
|
||||
|
||||
Reference in New Issue
Block a user