Files
openchamber/packages/ui/src/lib/runtimeSurface.ts
T

72 lines
2.4 KiB
TypeScript
Raw Normal View History

import { isDesktopShell, isVSCodeRuntime } from '@/lib/desktop';
import { isCapacitorApp } from '@/lib/platform';
import { getStoredMobileLayoutPreference } from '@/lib/mobileLayoutPreference';
2026-05-25 01:39:47 +03:00
export type HostedSurface = 'desktop' | 'mobile';
declare global {
interface Window {
__OPENCHAMBER_SURFACE__?: HostedSurface;
}
}
const MOBILE_SURFACE_MAX_WIDTH = 768;
const isTouchOrCoarsePointer = (): boolean => {
if (typeof window === 'undefined') return false;
const coarsePointer = typeof window.matchMedia === 'function'
? window.matchMedia('(pointer: coarse)').matches || window.matchMedia('(hover: none)').matches
: false;
const touchPoints = typeof navigator !== 'undefined' ? navigator.maxTouchPoints ?? 0 : 0;
return coarsePointer || touchPoints > 0;
};
/**
* 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.
*/
2026-08-13 15:30:54 +03:00
const detectHostedSurface = (): HostedSurface => {
2026-05-25 01:39:47 +03:00
if (typeof window === 'undefined') return 'desktop';
const explicitSurface = window.__OPENCHAMBER_SURFACE__;
if (explicitSurface === 'mobile' || explicitSurface === 'desktop') {
return explicitSurface;
}
const override = new URLSearchParams(window.location.search).get('surface');
if (override === 'mobile' || override === 'desktop') {
return override;
}
if (isCapacitorApp()) return 'mobile';
if (isDesktopShell() || isVSCodeRuntime()) return 'desktop';
2026-05-25 01:39:47 +03:00
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';
};
/**
* Decides the surface once and stamps it on `window` so every later
* `isMobileSurfaceRuntime()` call (perf tuning, sync paging, device info)
* reads the same stable answer instead of re-running viewport heuristics.
*/
export const resolveHostedSurface = (): HostedSurface => {
const surface = detectHostedSurface();
if (typeof window !== 'undefined') {
window.__OPENCHAMBER_SURFACE__ = surface;
}
return surface;
2026-05-25 01:39:47 +03:00
};
export const isMobileSurfaceRuntime = (): boolean => detectHostedSurface() === 'mobile';