feat(surface): switch app shells when the viewport crosses the phone threshold

The mobile-vs-desktop surface is stamped once at boot, so a browser
window narrowed past the phone threshold kept the desktop shell (and
its legacy squeezed layout) until a manual reload. A viewport watcher
now reloads into the other shell once the resize settles — the same
mechanism the old Settings toggle used. Fixed shells (Capacitor,
desktop, VS Code) and ?surface= overrides never switch.

With the new mobile app reachable this way, the old/new mobile layout
preference is gone: phones always get the mobile app.
This commit is contained in:
Bohdan Triapitsyn
2026-08-24 16:08:04 +03:00
parent 98c2a616c3
commit 841eca5720
16 changed files with 67 additions and 123 deletions
@@ -32,7 +32,6 @@ import { CODE_FONT_OPTIONS, DEFAULT_MONO_FONT, DEFAULT_UI_FONT, UI_FONT_OPTIONS,
import { useI18n, type Locale } from '@/lib/i18n';
import { useConfigStore } from '@/stores/useConfigStore';
import { normalizeMobileKeyboardMode, supportsMobileKeyboardResizeContent, type MobileKeyboardMode } from '@/lib/mobileKeyboardMode';
import { getStoredMobileLayoutPreference, setStoredMobileLayoutPreference, type MobileLayoutPreference } from '@/lib/mobileLayoutPreference';
import {
setDirectoryShowHidden,
useDirectoryShowHidden,
@@ -151,17 +150,6 @@ const MOBILE_KEYBOARD_MODE_OPTIONS: Option<MobileKeyboardMode>[] = [
},
];
const MOBILE_LAYOUT_OPTIONS: Array<{ value: MobileLayoutPreference; labelKey: string }> = [
{
value: 'default',
labelKey: 'settings.openchamber.visual.option.mobileLayout.default',
},
{
value: 'new',
labelKey: 'settings.openchamber.visual.option.mobileLayout.new',
},
];
type PwaInstallNameWindow = Window & {
__OPENCHAMBER_SET_PWA_INSTALL_NAME__?: (value: string) => string;
__OPENCHAMBER_SET_PWA_ORIENTATION__?: (value: 'system' | 'portrait' | 'landscape') => 'system' | 'portrait' | 'landscape';
@@ -629,10 +617,9 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
const hasThemeSettings = shouldShow('theme') && !isVSCode;
const showWindowControlsPositionSetting = shouldShow('windowControlsPosition') && showWindowControlsPosition;
const hasLocalizationSettings = shouldShow('theme') || shouldShow('timeFormat') || shouldShow('weekStart');
const showMobileLayoutSetting = isMobile && isWebRuntime() && !isDesktopShell() && !isVSCode;
const hasAppearanceSettings = isVSCode
? hasLocalizationSettings
: (shouldShow('theme') || showWindowControlsPositionSetting || showMobileLayoutSetting || shouldShow('pwaInstallName') || shouldShow('pwaOrientation') || shouldShow('timeFormat') || shouldShow('weekStart'));
: (shouldShow('theme') || showWindowControlsPositionSetting || shouldShow('pwaInstallName') || shouldShow('pwaOrientation') || shouldShow('timeFormat') || shouldShow('weekStart'));
const hasLayoutSettings = shouldShow('fontSize') || shouldShow('terminalFontSize') || shouldShow('editorFontSize') || shouldShow('spacing') || (shouldShow('inputBarOffset') && isMobile);
const hasNavigationSettings = (shouldShow('terminalQuickKeys') && !isMobile) || ((shouldShow('terminalShell') || shouldShow('terminalLoginShell')) && !isVSCode) || shouldShow('fileEditorKeymap') || shouldShow('autoSaveEnabled') || (shouldShow('expandedEditorToolbar') && !isVSCode);
const hasBehaviorSettings = shouldShow('mermaidRendering')
@@ -723,7 +710,6 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
? [...terminalLoginShells.filter((shell) => shell !== terminalShell), terminalShell]
: terminalLoginShells.filter((shell) => shell !== terminalShell));
};
const [mobileLayoutPreference, setMobileLayoutPreference] = React.useState<MobileLayoutPreference>(() => getStoredMobileLayoutPreference());
const [pwaInstallName, setPwaInstallName] = React.useState('');
const [pwaOrientation, setPwaOrientation] = React.useState<'system' | 'portrait' | 'landscape'>('system');
const selectedTimeFormatLabel = React.useMemo(() => {
@@ -743,16 +729,6 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
return option ? tUnsafe(option.labelKey) : undefined;
}, [mobileKeyboardMode, tUnsafe]);
const handleMobileLayoutPreferenceChange = React.useCallback((value: MobileLayoutPreference) => {
if (value === mobileLayoutPreference) {
return;
}
setMobileLayoutPreference(value);
setStoredMobileLayoutPreference(value);
window.location.reload();
}, [mobileLayoutPreference]);
const applyPwaInstallName = React.useCallback(async (value: string) => {
if (typeof window === 'undefined') {
return;
@@ -877,21 +853,6 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
))}
</SettingsRadioGroup>
{showMobileLayoutSetting && (
<SettingsInset>
<SettingsStackedField label={t('settings.openchamber.visual.section.mobileLayout')}>
<SettingsChipGroup
value={mobileLayoutPreference}
options={MOBILE_LAYOUT_OPTIONS.map((option) => ({
value: option.value,
label: tUnsafe(option.labelKey),
}))}
onChange={handleMobileLayoutPreferenceChange}
aria-label={t('settings.openchamber.visual.section.mobileLayout')}
/>
</SettingsStackedField>
</SettingsInset>
)}
</div>
<div className={SETTINGS_FIELDS_STACK_CLASS}>