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:
@@ -97,7 +97,7 @@
|
||||
},
|
||||
"packages/electron": {
|
||||
"name": "@openchamber/electron",
|
||||
"version": "1.19.0",
|
||||
"version": "1.20.0",
|
||||
"dependencies": {
|
||||
"@openchamber/web": "workspace:*",
|
||||
"electron-context-menu": "^4.1.2",
|
||||
@@ -134,7 +134,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@openchamber/ui",
|
||||
"version": "1.19.0",
|
||||
"version": "1.20.0",
|
||||
"dependencies": {
|
||||
"@aparajita/capacitor-secure-storage": "^8.0.0",
|
||||
"@base-ui/react": "^1.4.0",
|
||||
@@ -238,7 +238,7 @@
|
||||
},
|
||||
"packages/vscode": {
|
||||
"name": "openchamber",
|
||||
"version": "1.19.0",
|
||||
"version": "1.20.0",
|
||||
"dependencies": {
|
||||
"@openchamber/ui": "workspace:*",
|
||||
"@opencode-ai/sdk": "1.18.21",
|
||||
@@ -261,7 +261,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@openchamber/web",
|
||||
"version": "1.19.0",
|
||||
"version": "1.20.0",
|
||||
"bin": {
|
||||
"openchamber": "./bin/cli.js",
|
||||
},
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -1845,9 +1845,6 @@ export const settingsDict = {
|
||||
'settings.voice.page.field.ttsInputModeRaw': 'Rohes Markdown',
|
||||
'settings.voice.page.field.ttsInputModeSummarized': 'zusammengefasst',
|
||||
'settings.openchamber.visual.section.colorMode': 'Farbmodus',
|
||||
'settings.openchamber.visual.section.mobileLayout': 'Mobiles Layout',
|
||||
'settings.openchamber.visual.option.mobileLayout.default': 'Alt',
|
||||
'settings.openchamber.visual.option.mobileLayout.new': 'Neu',
|
||||
'settings.openchamber.visual.section.localization': 'Lokalisierung',
|
||||
'settings.openchamber.visual.section.spacingAndLayout': 'Abstand & Layout',
|
||||
'settings.openchamber.visual.section.navigation': 'Navigation',
|
||||
|
||||
@@ -1913,9 +1913,6 @@ export const settingsDict = {
|
||||
'settings.voice.page.field.ttsInputModeSummarized': 'summarized',
|
||||
'settings.openchamber.visual.section.colorMode': 'Color Mode',
|
||||
'settings.openchamber.visual.section.colorModeAndTheme': 'Color mode & Theme',
|
||||
'settings.openchamber.visual.section.mobileLayout': 'Mobile Layout',
|
||||
'settings.openchamber.visual.option.mobileLayout.default': 'Old',
|
||||
'settings.openchamber.visual.option.mobileLayout.new': 'New',
|
||||
'settings.openchamber.visual.section.localization': 'Localization',
|
||||
'settings.openchamber.visual.section.spacingAndLayout': 'Spacing & Layout',
|
||||
'settings.openchamber.visual.section.densityAndType': 'Density & type',
|
||||
|
||||
@@ -1890,9 +1890,6 @@ export const settingsDict = {
|
||||
"settings.voice.page.field.ttsInputModeSummarized": "resumido",
|
||||
"settings.openchamber.visual.section.colorMode": "Modo de color",
|
||||
"settings.openchamber.visual.section.colorModeAndTheme": "Modo de color y tema",
|
||||
"settings.openchamber.visual.section.mobileLayout": "Diseño móvil",
|
||||
"settings.openchamber.visual.option.mobileLayout.default": "Anterior",
|
||||
"settings.openchamber.visual.option.mobileLayout.new": "Nuevo",
|
||||
"settings.openchamber.visual.section.localization": "Localización",
|
||||
"settings.openchamber.visual.section.spacingAndLayout": "Espaciado y diseño",
|
||||
"settings.openchamber.visual.section.densityAndType": "Densidad y tipografía",
|
||||
|
||||
@@ -2147,9 +2147,6 @@ export const settingsDict = {
|
||||
'settings.voice.page.field.ttsInputModeSanitized': 'Nettoyé',
|
||||
'settings.voice.page.field.ttsInputModeRaw': 'Markdown brut',
|
||||
'settings.voice.page.field.ttsInputModeSummarized': 'résumé',
|
||||
'settings.openchamber.visual.section.mobileLayout': 'Mise en page mobile',
|
||||
'settings.openchamber.visual.option.mobileLayout.default': 'Ancienne',
|
||||
'settings.openchamber.visual.option.mobileLayout.new': 'Nouvelle',
|
||||
'settings.openchamber.visual.field.dockBadge': 'Badge du Dock',
|
||||
'settings.openchamber.visual.field.dockBadgeHint': 'Afficher sur l’icône du Dock de macOS le nombre de discussions avec une activité non vue.',
|
||||
'settings.openchamber.visual.actions.saveAndRestart': 'Enregistrer et redémarrer',
|
||||
|
||||
@@ -1923,9 +1923,6 @@ export const settingsDict = {
|
||||
'settings.voice.page.field.ttsInputModeSummarized': '要約',
|
||||
'settings.openchamber.visual.section.colorMode': 'カラーモード',
|
||||
'settings.openchamber.visual.section.colorModeAndTheme': 'カラーモードとテーマ',
|
||||
'settings.openchamber.visual.section.mobileLayout': 'モバイルレイアウト',
|
||||
'settings.openchamber.visual.option.mobileLayout.default': '旧',
|
||||
'settings.openchamber.visual.option.mobileLayout.new': '新',
|
||||
'settings.openchamber.visual.section.localization': 'ローカライゼーション',
|
||||
'settings.openchamber.visual.section.spacingAndLayout': '間隔とレイアウト',
|
||||
'settings.openchamber.visual.section.densityAndType': '密度と書体',
|
||||
|
||||
@@ -1890,9 +1890,6 @@ export const settingsDict = {
|
||||
'settings.voice.page.field.ttsInputModeSummarized': '요약',
|
||||
'settings.openchamber.visual.section.colorMode': '색상 모드',
|
||||
'settings.openchamber.visual.section.colorModeAndTheme': '색상 모드 및 테마',
|
||||
'settings.openchamber.visual.section.mobileLayout': '모바일 레이아웃',
|
||||
'settings.openchamber.visual.option.mobileLayout.default': '이전',
|
||||
'settings.openchamber.visual.option.mobileLayout.new': '새로움',
|
||||
'settings.openchamber.visual.section.localization': '지역화',
|
||||
'settings.openchamber.visual.section.spacingAndLayout': '간격 및 레이아웃',
|
||||
'settings.openchamber.visual.section.densityAndType': '밀도 및 서체',
|
||||
|
||||
@@ -1196,9 +1196,6 @@ export const settingsDict = {
|
||||
'settings.openchamber.visual.section.chatFeatures': 'Funkcje',
|
||||
'settings.openchamber.visual.section.colorMode': 'Tryb kolorów',
|
||||
'settings.openchamber.visual.section.colorModeAndTheme': 'Tryb kolorów i motyw',
|
||||
'settings.openchamber.visual.section.mobileLayout': 'Układ mobilny',
|
||||
'settings.openchamber.visual.option.mobileLayout.default': 'Poprzedni',
|
||||
'settings.openchamber.visual.option.mobileLayout.new': 'Nowy',
|
||||
'settings.openchamber.visual.section.diffLayout': 'Układ diffa',
|
||||
'settings.openchamber.visual.section.diffLayoutAria': 'Układ diffa',
|
||||
'settings.openchamber.visual.section.localization': 'Lokalizacja',
|
||||
|
||||
@@ -1890,9 +1890,6 @@ export const settingsDict = {
|
||||
"settings.voice.page.field.ttsInputModeSummarized": "resumido",
|
||||
"settings.openchamber.visual.section.colorMode": "Modo de cor",
|
||||
"settings.openchamber.visual.section.colorModeAndTheme": "Modo de cor e tema",
|
||||
"settings.openchamber.visual.section.mobileLayout": "Layout móvel",
|
||||
"settings.openchamber.visual.option.mobileLayout.default": "Anterior",
|
||||
"settings.openchamber.visual.option.mobileLayout.new": "Novo",
|
||||
"settings.openchamber.visual.section.localization": "Localização",
|
||||
"settings.openchamber.visual.section.spacingAndLayout": "Espaçamento e layout",
|
||||
"settings.openchamber.visual.section.densityAndType": "Densidade e tipografia",
|
||||
|
||||
@@ -1890,9 +1890,6 @@ export const settingsDict = {
|
||||
"settings.voice.page.field.ttsInputModeSummarized": "скорочений",
|
||||
"settings.openchamber.visual.section.colorMode": "Режим теми",
|
||||
"settings.openchamber.visual.section.colorModeAndTheme": "Режим кольору та тема",
|
||||
"settings.openchamber.visual.section.mobileLayout": "Мобільний макет",
|
||||
"settings.openchamber.visual.option.mobileLayout.default": "Попередній",
|
||||
"settings.openchamber.visual.option.mobileLayout.new": "Новий",
|
||||
"settings.openchamber.visual.section.localization": "Локалізація",
|
||||
"settings.openchamber.visual.section.spacingAndLayout": "Відступи й компонування",
|
||||
"settings.openchamber.visual.section.densityAndType": "Щільність і шрифти",
|
||||
|
||||
@@ -1890,9 +1890,6 @@ export const settingsDict = {
|
||||
'settings.voice.page.field.ttsInputModeSummarized': '摘要',
|
||||
'settings.openchamber.visual.section.colorMode': '颜色模式',
|
||||
'settings.openchamber.visual.section.colorModeAndTheme': '颜色模式与主题',
|
||||
'settings.openchamber.visual.section.mobileLayout': '移动端布局',
|
||||
'settings.openchamber.visual.option.mobileLayout.default': '旧版',
|
||||
'settings.openchamber.visual.option.mobileLayout.new': '新版',
|
||||
'settings.openchamber.visual.section.localization': '本地化',
|
||||
'settings.openchamber.visual.section.spacingAndLayout': '间距与布局',
|
||||
'settings.openchamber.visual.section.densityAndType': '密度与字体',
|
||||
|
||||
@@ -1801,7 +1801,6 @@ export const settingsDict = {
|
||||
'settings.openchamber.visual.section.spacingAndLayout': '間距與佈局',
|
||||
'settings.openchamber.visual.section.densityAndType': '密度與字型',
|
||||
'settings.openchamber.visual.section.appInstall': '應用程式安裝',
|
||||
'settings.openchamber.visual.section.mobileLayout': '行動版版面',
|
||||
'settings.openchamber.visual.section.navigation': '導覽',
|
||||
'settings.openchamber.visual.section.chatRenderMode': '聊天渲染模式',
|
||||
'settings.openchamber.visual.section.chatRenderModeAria': '聊天渲染模式',
|
||||
@@ -1855,8 +1854,6 @@ export const settingsDict = {
|
||||
'settings.openchamber.visual.field.mobileKeyboardModeAria': '行動裝置鍵盤行為',
|
||||
'settings.openchamber.visual.field.selectMobileKeyboardModePlaceholder': '選擇鍵盤行為',
|
||||
'settings.openchamber.visual.actions.resetMobileKeyboardModeAria': '重設行動裝置鍵盤行為',
|
||||
'settings.openchamber.visual.option.mobileLayout.default': '舊版',
|
||||
'settings.openchamber.visual.option.mobileLayout.new': '新版',
|
||||
'settings.openchamber.visual.field.interfaceFontSize': '介面字體大小',
|
||||
'settings.openchamber.visual.field.interfaceFont': '介面字體',
|
||||
'settings.openchamber.visual.field.selectInterfaceFontAria': '選擇介面字體',
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
export type MobileLayoutPreference = 'default' | 'new';
|
||||
|
||||
const MOBILE_LAYOUT_PREFERENCE_KEY = 'openchamber-mobile-layout';
|
||||
|
||||
const normalizeMobileLayoutPreference = (value: unknown): MobileLayoutPreference => {
|
||||
// 'new' is the default; only an explicit 'default' (the legacy/"Old" layout)
|
||||
// opts out of it.
|
||||
return value === 'default' ? 'default' : 'new';
|
||||
};
|
||||
|
||||
export const getStoredMobileLayoutPreference = (): MobileLayoutPreference => {
|
||||
if (typeof window === 'undefined') {
|
||||
return 'new';
|
||||
}
|
||||
|
||||
try {
|
||||
return normalizeMobileLayoutPreference(window.localStorage.getItem(MOBILE_LAYOUT_PREFERENCE_KEY));
|
||||
} catch {
|
||||
return 'new';
|
||||
}
|
||||
};
|
||||
|
||||
export const setStoredMobileLayoutPreference = (value: MobileLayoutPreference): boolean => {
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
window.localStorage.setItem(MOBILE_LAYOUT_PREFERENCE_KEY, value);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
import { isDesktopShell, isVSCodeRuntime } from '@/lib/desktop';
|
||||
import { isCapacitorApp } from '@/lib/platform';
|
||||
import { getStoredMobileLayoutPreference } from '@/lib/mobileLayoutPreference';
|
||||
|
||||
export type HostedSurface = 'desktop' | 'mobile';
|
||||
|
||||
@@ -11,6 +10,7 @@ declare global {
|
||||
}
|
||||
|
||||
const MOBILE_SURFACE_MAX_WIDTH = 768;
|
||||
const SURFACE_SWITCH_DEBOUNCE_MS = 800;
|
||||
|
||||
const isTouchOrCoarsePointer = (): boolean => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
@@ -22,12 +22,29 @@ const isTouchOrCoarsePointer = (): boolean => {
|
||||
return coarsePointer || touchPoints > 0;
|
||||
};
|
||||
|
||||
const hasSurfaceUrlOverride = (): boolean => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
const override = new URLSearchParams(window.location.search).get('surface');
|
||||
return override === 'mobile' || override === 'desktop';
|
||||
};
|
||||
|
||||
/** Viewport half of the surface decision; re-evaluated on resize by the watcher. */
|
||||
const isPhoneViewport = (): boolean => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
const width = Math.min(
|
||||
window.innerWidth || Number.POSITIVE_INFINITY,
|
||||
window.screen?.width || Number.POSITIVE_INFINITY,
|
||||
);
|
||||
return Number.isFinite(width)
|
||||
&& width <= MOBILE_SURFACE_MAX_WIDTH
|
||||
&& isTouchOrCoarsePointer();
|
||||
};
|
||||
|
||||
/**
|
||||
* Single authority for the mobile-vs-desktop surface decision.
|
||||
*
|
||||
* Priority: explicit stamp (set once at boot) → URL override → Capacitor
|
||||
* shell (always the mobile surface) → desktop shells → phone heuristic
|
||||
* gated by the stored mobile layout preference.
|
||||
* shell (always the mobile surface) → desktop shells → phone heuristic.
|
||||
*/
|
||||
const detectHostedSurface = (): HostedSurface => {
|
||||
if (typeof window === 'undefined') return 'desktop';
|
||||
@@ -45,14 +62,7 @@ const detectHostedSurface = (): HostedSurface => {
|
||||
if (isCapacitorApp()) return 'mobile';
|
||||
if (isDesktopShell() || isVSCodeRuntime()) return 'desktop';
|
||||
|
||||
const width = Math.min(
|
||||
window.innerWidth || Number.POSITIVE_INFINITY,
|
||||
window.screen?.width || Number.POSITIVE_INFINITY,
|
||||
);
|
||||
const likelyPhone = Number.isFinite(width)
|
||||
&& width <= MOBILE_SURFACE_MAX_WIDTH
|
||||
&& isTouchOrCoarsePointer();
|
||||
return likelyPhone && getStoredMobileLayoutPreference() === 'new' ? 'mobile' : 'desktop';
|
||||
return isPhoneViewport() ? 'mobile' : 'desktop';
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -69,3 +79,39 @@ export const resolveHostedSurface = (): HostedSurface => {
|
||||
};
|
||||
|
||||
export const isMobileSurfaceRuntime = (): boolean => detectHostedSurface() === 'mobile';
|
||||
|
||||
/**
|
||||
* The surface is stamped once at boot, so a browser window that crosses the
|
||||
* phone threshold after load would otherwise keep the wrong app shell (the
|
||||
* app trees, stores, and sync bootstrap differ, so an in-place switch is not
|
||||
* safe). Watch for the viewport heuristic disagreeing with the stamp and
|
||||
* reload — the same mechanism a surface change has always used — once the
|
||||
* resize settles. Fixed shells never switch: Capacitor is always mobile,
|
||||
* desktop/VS Code shells are always desktop, and an explicit ?surface=
|
||||
* override wins over the heuristic.
|
||||
*/
|
||||
export const watchHostedSurfaceViewport = (): (() => void) => {
|
||||
if (typeof window === 'undefined') return () => {};
|
||||
if (isCapacitorApp() || isDesktopShell() || isVSCodeRuntime()) return () => {};
|
||||
if (hasSurfaceUrlOverride()) return () => {};
|
||||
|
||||
let timeout: ReturnType<typeof setTimeout> | null = null;
|
||||
const handleResize = () => {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
timeout = null;
|
||||
const stamped = window.__OPENCHAMBER_SURFACE__;
|
||||
const desired: HostedSurface = isPhoneViewport() ? 'mobile' : 'desktop';
|
||||
if (stamped && stamped !== desired) {
|
||||
window.__OPENCHAMBER_SURFACE__ = undefined;
|
||||
window.location.reload();
|
||||
}
|
||||
}, SURFACE_SWITCH_DEBOUNCE_MS);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createConfiguredWebAPIs, getDesktopRelayRestoreReady } from './runtimeC
|
||||
import { registerSW } from 'virtual:pwa-register';
|
||||
|
||||
import type { RuntimeAPIs } from '@openchamber/ui/lib/api/types';
|
||||
import { resolveHostedSurface, type HostedSurface } from '@openchamber/ui/lib/runtimeSurface';
|
||||
import { resolveHostedSurface, watchHostedSurfaceViewport, type HostedSurface } from '@openchamber/ui/lib/runtimeSurface';
|
||||
import {
|
||||
isEmbeddedSessionChat,
|
||||
requestEmbeddedSessionRuntimeBootstrap,
|
||||
@@ -90,6 +90,10 @@ const start = async (): Promise<void> => {
|
||||
: null;
|
||||
window.__OPENCHAMBER_RUNTIME_APIS__ = createConfiguredWebAPIs(embeddedBootstrap);
|
||||
|
||||
// Reload into the other app shell when the viewport crosses the phone
|
||||
// threshold after boot (no-op in fixed shells and with ?surface= overrides).
|
||||
watchHostedSurfaceViewport();
|
||||
|
||||
if (hostedSurface === 'mobile') {
|
||||
const { renderMobileApp } = await import('@openchamber/ui/apps/renderMobileApp');
|
||||
renderMobileApp(window.__OPENCHAMBER_RUNTIME_APIS__);
|
||||
|
||||
Reference in New Issue
Block a user