fix: separate platform detection from responsive design, fix boot screen crash on web

The boot screen was rendering as a black screen on web because
useNativeFeatures() called Capacitor's Network.getStatus() without
checking if the app was running on a native platform. This crashed the
entire AeThexOS component during mount.

Additionally, tablet testing code in use-platform-layout.ts was mixing
viewport width checks (responsive design) with native platform detection,
causing layout confusion between web and mobile builds.

Changes:
- Add isMobile() guards to all Capacitor plugin calls in useNativeFeatures
- Remove tablet viewport-width branch from usePlatformLayout (platform
  detection should not check window.innerWidth)
- Rename isMobileDevice() to isSmallViewport() in embed-utils to clarify
  it's a responsive check, not a platform check
- Rename local isMobile state to isNarrowViewport in os.tsx DesktopWidgets
  to prevent shadowing the platform.ts isMobile() import
- Remove dead PlatformAdaptiveExample.tsx (not imported anywhere)
- Fix watchLocation TypeScript error (watchId is Promise<string>)
- Add web fallbacks for clipboard and browser in useNativeFeatures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
MrPiglr 2026-02-14 00:09:06 -07:00
parent 33e0a26d35
commit 51ed8371b9
5 changed files with 60 additions and 201 deletions

View file

@ -1,163 +0,0 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { usePlatformLayout, usePlatformClasses, PlatformSwitch } from '@/hooks/use-platform-layout';
import { Home, Users, Settings, Plus } from 'lucide-react';
/**
* Example component showing how to adapt UI for different platforms
*/
export function PlatformAdaptiveExample() {
const layout = usePlatformLayout();
const classes = usePlatformClasses();
return (
<div className={classes.container}>
{/* Platform-specific header */}
<PlatformSwitch
mobile={<MobileHeader />}
desktop={<DesktopHeader />}
web={<WebHeader />}
/>
{/* Content that adapts to platform */}
<div className={classes.spacing}>
<Card className={classes.card}>
<CardHeader>
<CardTitle className={classes.heading}>
Platform: {layout.isMobile ? 'Mobile' : layout.isDesktop ? 'Desktop' : 'Web'}
</CardTitle>
</CardHeader>
<CardContent className={classes.spacing}>
<p className={classes.fontSize}>
This component automatically adapts its layout and styling based on the platform.
</p>
{/* Platform-specific buttons */}
<div className="flex gap-2">
<Button className={classes.button}>
<Plus className="mr-2 h-4 w-4" />
{layout.isMobile ? 'Add' : 'Add New Item'}
</Button>
</div>
</CardContent>
</Card>
{/* Grid that adapts to screen size and platform */}
<div className={`grid gap-4 ${
layout.isMobile ? 'grid-cols-1' :
layout.isDesktop ? 'grid-cols-3' :
'grid-cols-2'
}`}>
<Card className={classes.card}>
<CardContent className="pt-6">
<Home className="h-8 w-8 mb-2" />
<h3 className={classes.subheading}>Dashboard</h3>
</CardContent>
</Card>
<Card className={classes.card}>
<CardContent className="pt-6">
<Users className="h-8 w-8 mb-2" />
<h3 className={classes.subheading}>Team</h3>
</CardContent>
</Card>
<Card className={classes.card}>
<CardContent className="pt-6">
<Settings className="h-8 w-8 mb-2" />
<h3 className={classes.subheading}>Settings</h3>
</CardContent>
</Card>
</div>
</div>
{/* Platform-specific navigation */}
<PlatformSwitch
mobile={<MobileBottomNav />}
desktop={<DesktopTopNav />}
web={<WebStickyNav />}
/>
</div>
);
}
// Mobile: Bottom navigation bar
function MobileBottomNav() {
return (
<nav className="fixed bottom-0 left-0 right-0 bg-background border-t">
<div className="flex justify-around items-center h-16 px-4">
<NavItem icon={<Home />} label="Home" />
<NavItem icon={<Users />} label="Team" />
<NavItem icon={<Settings />} label="Settings" />
</div>
</nav>
);
}
// Desktop: Top navigation bar
function DesktopTopNav() {
return (
<nav className="fixed top-0 left-0 right-0 bg-background border-b">
<div className="flex items-center justify-between h-16 px-8">
<div className="flex items-center gap-8">
<span className="text-xl font-bold">AeThex OS</span>
<NavItem icon={<Home />} label="Dashboard" />
<NavItem icon={<Users />} label="Team" />
</div>
<NavItem icon={<Settings />} label="Settings" />
</div>
</nav>
);
}
// Web: Sticky navigation
function WebStickyNav() {
return (
<nav className="sticky top-0 bg-background/95 backdrop-blur border-b z-50">
<div className="flex items-center justify-between h-14 px-6">
<div className="flex items-center gap-6">
<span className="text-lg font-bold">AeThex OS</span>
<NavItem icon={<Home />} label="Home" />
<NavItem icon={<Users />} label="Team" />
</div>
<NavItem icon={<Settings />} label="Settings" />
</div>
</nav>
);
}
function NavItem({ icon, label }: { icon: React.ReactNode; label: string }) {
return (
<button className="flex flex-col items-center gap-1 text-muted-foreground hover:text-foreground transition-colors">
{icon}
<span className="text-xs">{label}</span>
</button>
);
}
// Mobile-specific header
function MobileHeader() {
return (
<header className="sticky top-0 bg-background border-b z-10 px-4 py-3">
<h1 className="text-xl font-bold">AeThex OS</h1>
</header>
);
}
// Desktop-specific header
function DesktopHeader() {
return (
<header className="mb-6">
<h1 className="text-3xl font-bold mb-2">AeThex OS Desktop</h1>
<p className="text-muted-foreground">Native desktop experience</p>
</header>
);
}
// Web-specific header
function WebHeader() {
return (
<header className="mb-4">
<h1 className="text-2xl font-bold mb-1">AeThex OS</h1>
<p className="text-sm text-muted-foreground">Web desktop platform</p>
</header>
);
}

View file

@ -12,6 +12,7 @@ import { ScreenOrientation } from '@capacitor/screen-orientation';
import { Browser } from '@capacitor/browser'; import { Browser } from '@capacitor/browser';
import { App } from '@capacitor/app'; import { App } from '@capacitor/app';
import { Haptics, ImpactStyle } from '@capacitor/haptics'; import { Haptics, ImpactStyle } from '@capacitor/haptics';
import { isMobile } from '@/lib/platform';
interface UseNativeFeaturesReturn { interface UseNativeFeaturesReturn {
// Camera // Camera
@ -59,19 +60,25 @@ interface UseNativeFeaturesReturn {
export function useNativeFeatures(): UseNativeFeaturesReturn { export function useNativeFeatures(): UseNativeFeaturesReturn {
const [networkStatus, setNetworkStatus] = useState({ connected: true, connectionType: 'unknown' }); const [networkStatus, setNetworkStatus] = useState({ connected: true, connectionType: 'unknown' });
// Initialize network monitoring // Initialize network monitoring - only on native platforms
useEffect(() => { useEffect(() => {
if (!isMobile()) return;
const initNetwork = async () => { const initNetwork = async () => {
const status = await Network.getStatus(); try {
setNetworkStatus({ connected: status.connected, connectionType: status.connectionType }); const status = await Network.getStatus();
Network.addListener('networkStatusChange', status => {
setNetworkStatus({ connected: status.connected, connectionType: status.connectionType }); setNetworkStatus({ connected: status.connected, connectionType: status.connectionType });
});
Network.addListener('networkStatusChange', status => {
setNetworkStatus({ connected: status.connected, connectionType: status.connectionType });
});
} catch (error) {
console.debug('[NativeFeatures] Network init failed:', error);
}
}; };
initNetwork(); initNetwork();
return () => { return () => {
Network.removeAllListeners(); Network.removeAllListeners();
}; };
@ -79,6 +86,7 @@ export function useNativeFeatures(): UseNativeFeaturesReturn {
// Camera functions // Camera functions
const takePhoto = async (): Promise<string | null> => { const takePhoto = async (): Promise<string | null> => {
if (!isMobile()) return null;
try { try {
const image = await Camera.getPhoto({ const image = await Camera.getPhoto({
quality: 90, quality: 90,
@ -94,6 +102,7 @@ export function useNativeFeatures(): UseNativeFeaturesReturn {
}; };
const pickPhoto = async (): Promise<string | null> => { const pickPhoto = async (): Promise<string | null> => {
if (!isMobile()) return null;
try { try {
const image = await Camera.getPhoto({ const image = await Camera.getPhoto({
quality: 90, quality: 90,
@ -110,6 +119,7 @@ export function useNativeFeatures(): UseNativeFeaturesReturn {
// File system functions // File system functions
const saveFile = async (data: string, filename: string): Promise<boolean> => { const saveFile = async (data: string, filename: string): Promise<boolean> => {
if (!isMobile()) return false;
try { try {
await Filesystem.writeFile({ await Filesystem.writeFile({
path: filename, path: filename,
@ -126,6 +136,7 @@ export function useNativeFeatures(): UseNativeFeaturesReturn {
}; };
const readFile = async (filename: string): Promise<string | null> => { const readFile = async (filename: string): Promise<string | null> => {
if (!isMobile()) return null;
try { try {
const result = await Filesystem.readFile({ const result = await Filesystem.readFile({
path: filename, path: filename,
@ -140,14 +151,14 @@ export function useNativeFeatures(): UseNativeFeaturesReturn {
}; };
const pickFile = async (): Promise<string | null> => { const pickFile = async (): Promise<string | null> => {
// Note: Capacitor doesn't have a built-in file picker if (!isMobile()) return null;
// You'd need to use a plugin like @capacitor-community/file-picker
console.log('File picker not implemented - need @capacitor-community/file-picker'); console.log('File picker not implemented - need @capacitor-community/file-picker');
return null; return null;
}; };
// Share functions // Share functions
const shareText = async (text: string, title?: string): Promise<void> => { const shareText = async (text: string, title?: string): Promise<void> => {
if (!isMobile()) return;
try { try {
await Share.share({ await Share.share({
text: text, text: text,
@ -161,6 +172,7 @@ export function useNativeFeatures(): UseNativeFeaturesReturn {
}; };
const shareUrl = async (url: string, title?: string): Promise<void> => { const shareUrl = async (url: string, title?: string): Promise<void> => {
if (!isMobile()) return;
try { try {
await Share.share({ await Share.share({
url: url, url: url,
@ -175,6 +187,7 @@ export function useNativeFeatures(): UseNativeFeaturesReturn {
// Notification functions // Notification functions
const requestNotificationPermission = async (): Promise<boolean> => { const requestNotificationPermission = async (): Promise<boolean> => {
if (!isMobile()) return false;
try { try {
const result = await PushNotifications.requestPermissions(); const result = await PushNotifications.requestPermissions();
if (result.receive === 'granted') { if (result.receive === 'granted') {
@ -189,6 +202,7 @@ export function useNativeFeatures(): UseNativeFeaturesReturn {
}; };
const sendLocalNotification = async (title: string, body: string): Promise<void> => { const sendLocalNotification = async (title: string, body: string): Promise<void> => {
if (!isMobile()) return;
try { try {
await LocalNotifications.schedule({ await LocalNotifications.schedule({
notifications: [ notifications: [
@ -211,6 +225,7 @@ export function useNativeFeatures(): UseNativeFeaturesReturn {
// Location functions // Location functions
const getCurrentLocation = async (): Promise<Position | null> => { const getCurrentLocation = async (): Promise<Position | null> => {
if (!isMobile()) return null;
try { try {
const position = await Geolocation.getCurrentPosition({ const position = await Geolocation.getCurrentPosition({
enableHighAccuracy: true, enableHighAccuracy: true,
@ -224,20 +239,27 @@ export function useNativeFeatures(): UseNativeFeaturesReturn {
}; };
const watchLocation = (callback: (position: Position) => void) => { const watchLocation = (callback: (position: Position) => void) => {
const watchId = Geolocation.watchPosition( if (!isMobile()) return () => {};
let watchIdPromise: Promise<string> | null = null;
watchIdPromise = Geolocation.watchPosition(
{ enableHighAccuracy: true }, { enableHighAccuracy: true },
(position, err) => { (position, err) => {
if (position) callback(position); if (position) callback(position);
} }
); );
return () => { return () => {
if (watchId) Geolocation.clearWatch({ id: watchId }); watchIdPromise?.then(id => Geolocation.clearWatch({ id }));
}; };
}; };
// Clipboard functions // Clipboard functions
const copyToClipboard = async (text: string): Promise<void> => { const copyToClipboard = async (text: string): Promise<void> => {
if (!isMobile()) {
// Fallback to web clipboard API
try { await navigator.clipboard.writeText(text); } catch {}
return;
}
try { try {
await Clipboard.write({ string: text }); await Clipboard.write({ string: text });
await showToast('Copied to clipboard', 'short'); await showToast('Copied to clipboard', 'short');
@ -248,6 +270,10 @@ export function useNativeFeatures(): UseNativeFeaturesReturn {
}; };
const pasteFromClipboard = async (): Promise<string> => { const pasteFromClipboard = async (): Promise<string> => {
if (!isMobile()) {
// Fallback to web clipboard API
try { return await navigator.clipboard.readText(); } catch { return ''; }
}
try { try {
const result = await Clipboard.read(); const result = await Clipboard.read();
return result.value; return result.value;
@ -259,6 +285,7 @@ export function useNativeFeatures(): UseNativeFeaturesReturn {
// Screen orientation // Screen orientation
const lockOrientation = async (orientation: 'portrait' | 'landscape'): Promise<void> => { const lockOrientation = async (orientation: 'portrait' | 'landscape'): Promise<void> => {
if (!isMobile()) return;
try { try {
await ScreenOrientation.lock({ orientation: orientation }); await ScreenOrientation.lock({ orientation: orientation });
} catch (error) { } catch (error) {
@ -267,6 +294,7 @@ export function useNativeFeatures(): UseNativeFeaturesReturn {
}; };
const unlockOrientation = async (): Promise<void> => { const unlockOrientation = async (): Promise<void> => {
if (!isMobile()) return;
try { try {
await ScreenOrientation.unlock(); await ScreenOrientation.unlock();
} catch (error) { } catch (error) {
@ -276,6 +304,11 @@ export function useNativeFeatures(): UseNativeFeaturesReturn {
// Browser // Browser
const openInBrowser = async (url: string): Promise<void> => { const openInBrowser = async (url: string): Promise<void> => {
if (!isMobile()) {
// Fallback to web: open in new tab
window.open(url, '_blank');
return;
}
try { try {
await Browser.open({ url }); await Browser.open({ url });
} catch (error) { } catch (error) {
@ -285,6 +318,7 @@ export function useNativeFeatures(): UseNativeFeaturesReturn {
// Toast // Toast
const showToast = async (text: string, duration: 'short' | 'long' = 'short'): Promise<void> => { const showToast = async (text: string, duration: 'short' | 'long' = 'short'): Promise<void> => {
if (!isMobile()) return;
try { try {
await Toast.show({ await Toast.show({
text: text, text: text,
@ -298,6 +332,7 @@ export function useNativeFeatures(): UseNativeFeaturesReturn {
// Haptics // Haptics
const vibrate = async (style: 'light' | 'medium' | 'heavy' = 'medium'): Promise<void> => { const vibrate = async (style: 'light' | 'medium' | 'heavy' = 'medium'): Promise<void> => {
if (!isMobile()) return;
try { try {
const styleMap = { const styleMap = {
light: ImpactStyle.Light, light: ImpactStyle.Light,

View file

@ -27,24 +27,9 @@ export function usePlatformLayout(): LayoutConfig {
const config = useMemo((): LayoutConfig => { const config = useMemo((): LayoutConfig => {
if (platformCheck.isMobile) { if (platformCheck.isMobile) {
// Tablet Optimization: Check if screen is large enough (e.g. iPad/Tablet)
const isTablet = typeof window !== 'undefined' && window.innerWidth >= 768;
if (isTablet) {
return {
...platformCheck,
// Tablet styling (Hybrid)
containerClass: 'px-6 py-4 max-w-2xl mx-auto', // Centered content for tablets
cardClass: 'rounded-xl shadow-md border p-5',
navClass: 'fixed bottom-0 left-0 right-0 bg-background/95 backdrop-blur border-t z-50',
spacing: 'space-y-4',
fontSize: 'text-base',
};
}
return { return {
...platformCheck, ...platformCheck,
// Mobile-first container styling (Phone) // Native mobile app styling (Capacitor/Flutter/Cordova)
containerClass: 'px-4 py-3 max-w-full', containerClass: 'px-4 py-3 max-w-full',
cardClass: 'rounded-lg shadow-sm border p-4', cardClass: 'rounded-lg shadow-sm border p-4',
navClass: 'fixed bottom-0 left-0 right-0 bg-background border-t', navClass: 'fixed bottom-0 left-0 right-0 bg-background border-t',

View file

@ -12,9 +12,11 @@ export const isEmbedded = (): boolean => {
}; };
/** /**
* Detect if running on mobile device * Detect if viewport is small (phone-sized).
* This is a responsive/viewport check, NOT a platform check.
* For native platform detection, use isMobile() from '@/lib/platform'.
*/ */
export const isMobileDevice = (): boolean => { export const isSmallViewport = (): boolean => {
return typeof window !== 'undefined' && window.innerWidth < 768; return typeof window !== 'undefined' && window.innerWidth < 768;
}; };
@ -58,7 +60,7 @@ export const getMobileTheme = () => {
*/ */
export const getResponsiveStyles = () => { export const getResponsiveStyles = () => {
const embedded = isEmbedded(); const embedded = isEmbedded();
const mobile = isMobileDevice(); const mobile = isSmallViewport();
const theme = getMobileTheme(); const theme = getMobileTheme();
return { return {

View file

@ -2034,13 +2034,13 @@ function DesktopWidgets({ time, weather, notifications }: {
}); });
const [showWidgetSettings, setShowWidgetSettings] = useState(false); const [showWidgetSettings, setShowWidgetSettings] = useState(false);
const [mobileWidgetsOpen, setMobileWidgetsOpen] = useState(false); const [mobileWidgetsOpen, setMobileWidgetsOpen] = useState(false);
const [isMobile, setIsMobile] = useState(false); const [isNarrowViewport, setIsNarrowViewport] = useState(false);
useEffect(() => { useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth < 768); const checkViewport = () => setIsNarrowViewport(window.innerWidth < 768);
checkMobile(); checkViewport();
window.addEventListener('resize', checkMobile); window.addEventListener('resize', checkViewport);
return () => window.removeEventListener('resize', checkMobile); return () => window.removeEventListener('resize', checkViewport);
}, []); }, []);
const toggleWidgetVisibility = (id: string) => { const toggleWidgetVisibility = (id: string) => {
@ -2115,7 +2115,7 @@ function DesktopWidgets({ time, weather, notifications }: {
return { color: 'text-cyan-400', icon: <Users className="w-3 h-3" /> }; return { color: 'text-cyan-400', icon: <Users className="w-3 h-3" /> };
}; };
if (isMobile) { if (isNarrowViewport) {
return ( return (
<> <>
<button <button