Centralize platform gating and fix iPad/tablet support

Add `packages/app/src/constants/platform.ts` with canonical gates
(isWeb, isNative, getIsElectron) and refactor all ~100 ad-hoc
Platform.OS checks across 69 files to use shared imports.

Fix sidebar hover crash on native (onPointerEnter → onHoverIn),
fix tooltip to use breakpoint instead of platform detection, make
sidebar action buttons always visible on native where hover doesn't
work, and document the platform gating decision matrix in CLAUDE.md.
This commit is contained in:
Mohamed Boudra
2026-04-13 13:24:00 +07:00
parent 964bf3e13f
commit 0f7fa55a0f
70 changed files with 551 additions and 515 deletions
+7 -6
View File
@@ -81,6 +81,7 @@ import {
parseWorkspaceOpenIntent,
} from "@/utils/host-routes";
import { syncNavigationActiveWorkspace } from "@/stores/navigation-active-workspace-store";
import { isWeb, isNative } from "@/constants/platform";
polyfillCrypto();
@@ -101,7 +102,7 @@ function PushNotificationRouter() {
const lastHandledIdRef = useRef<string | null>(null);
useEffect(() => {
if (Platform.OS === "web") {
if (isWeb) {
let removeDesktopNotificationListener: (() => void) | null = null;
let cancelled = false;
@@ -390,10 +391,10 @@ function AppContainer({
const screenW = UnistylesRuntime.screen.width;
const screenH = UnistylesRuntime.screen.height;
const isElectron = getIsElectronRuntime();
const windowW = Platform.OS === "web" ? window.innerWidth : undefined;
const windowH = Platform.OS === "web" ? window.innerHeight : undefined;
const dpr = Platform.OS === "web" ? window.devicePixelRatio : undefined;
const ua = Platform.OS === "web" ? navigator.userAgent : undefined;
const windowW = isWeb ? window.innerWidth : undefined;
const windowH = isWeb ? window.innerHeight : undefined;
const dpr = isWeb ? window.devicePixelRatio : undefined;
const ua = isWeb ? navigator.userAgent : undefined;
console.log(
"[layout-debug]",
@@ -577,7 +578,7 @@ function ProvidersWrapper({ children }: { children: ReactNode }) {
}, [settingsLoading, settings.theme]);
useEffect(() => {
if (settingsLoading || Platform.OS !== "web") {
if (settingsLoading || isNative) {
return;
}
@@ -1,6 +1,5 @@
import { useEffect, useRef, useState } from "react";
import { useGlobalSearchParams, useLocalSearchParams, useRootNavigationState } from "expo-router";
import { Platform } from "react-native";
import { HostRouteBootstrapBoundary } from "@/components/host-route-bootstrap-boundary";
import type { WorkspaceTabTarget } from "@/stores/workspace-tabs-store";
import { WorkspaceScreen } from "@/screens/workspace/workspace-screen";
@@ -10,6 +9,7 @@ import {
type WorkspaceOpenIntent,
} from "@/utils/host-routes";
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
import { isWeb } from "@/constants/platform";
function getParamValue(value: string | string[] | undefined): string {
if (typeof value === "string") {
@@ -88,7 +88,7 @@ function HostWorkspaceLayoutContent() {
// Expo Router's replace ignores query-param-only changes (findDivergentState
// skips search params). Strip ?open from the browser URL directly so the
// address bar reflects the clean workspace route.
if (Platform.OS === "web" && typeof window !== "undefined") {
if (isWeb && typeof window !== "undefined") {
const url = new URL(window.location.href);
if (url.searchParams.has("open")) {
url.searchParams.delete("open");
+4 -3
View File
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { Alert, Platform, Pressable, Text, View } from "react-native";
import { Alert, Pressable, Text, View } from "react-native";
import { useLocalSearchParams, useRouter } from "expo-router";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
@@ -10,6 +10,7 @@ import { decodeOfferFragmentPayload, normalizeHostPort } from "@/utils/daemon-en
import { connectToDaemon } from "@/utils/test-daemon-connection";
import { ConnectionOfferSchema } from "@server/shared/connection-offer";
import { buildHostRootRoute, buildHostSettingsRoute } from "@/utils/host-routes";
import { isWeb } from "@/constants/platform";
const styles = StyleSheet.create((theme) => ({
container: {
@@ -198,7 +199,7 @@ export default function PairScanScreen() {
}, [router, source, sourceServerId, targetServerId]);
useEffect(() => {
if (Platform.OS === "web") return;
if (isWeb) return;
if (permission && permission.granted) return;
void requestPermission().catch(() => undefined);
}, [permission, requestPermission]);
@@ -253,7 +254,7 @@ export default function PairScanScreen() {
[daemons, isPairing, returnToSource, targetServerId, upsertDaemonFromOfferUrl],
);
if (Platform.OS === "web") {
if (isWeb) {
return (
<View style={styles.container}>
<View style={[styles.header, { paddingTop: insets.top + theme.spacing[2] }]}>