diff --git a/packages/mobile-client/App.tsx b/packages/mobile-client/App.tsx
index 4f2af06..c9f607a 100644
--- a/packages/mobile-client/App.tsx
+++ b/packages/mobile-client/App.tsx
@@ -1,9 +1,9 @@
import React, { useState, useEffect } from 'react';
import { StatusBar } from 'expo-status-bar';
-import { StyleSheet, View, SafeAreaView } from 'react-native';
+import { StyleSheet, View } from 'react-native';
+import { SafeAreaProvider } from 'react-native-safe-area-context';
import { Provider as PaperProvider, BottomNavigation, Badge } from 'react-native-paper';
import { MaterialCommunityIcons } from '@expo/vector-icons';
-import * as Haptics from 'expo-haptics';
import type { InjectPromptResponse } from '@codelink/protocol';
// Components
@@ -40,6 +40,12 @@ import { useCustomFonts } from './src/design-system/typography/fontLoading';
// Utils
import { isInjectPromptResponse, isSyncFullContextMessage } from './src/utils/messageValidation';
+import {
+ getStatusBarStyle,
+ registerBackButtonHandler,
+ isAndroid,
+ triggerHapticFeedback,
+} from './src/utils/platformAdaptations';
/**
* Main application content with navigation
@@ -94,11 +100,11 @@ const AppContent: React.FC = () => {
});
}
- // Haptic feedback
+ // Haptic feedback (Requirements 22.3, 22.4)
if (response.payload.success) {
- Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
+ triggerHapticFeedback('success');
} else {
- Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
+ triggerHapticFeedback('error');
}
// Clear response after 4 seconds
@@ -129,17 +135,36 @@ const AppContent: React.FC = () => {
} catch (error) {
setIsSubmitting(false);
setPromptError(error instanceof Error ? error.message : 'Failed to submit prompt');
- Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
+ triggerHapticFeedback('error');
}
};
// Handle reconnection (currently unused but kept for future use)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _handleReconnect = async () => {
- await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
+ await triggerHapticFeedback('medium');
reconnect();
};
+ // Handle Android hardware back button
+ // Requirement 26.7: Android hardware back button support
+ useEffect(() => {
+ if (!isAndroid()) return;
+
+ const backHandler = () => {
+ // If not on dashboard, go back to dashboard
+ if (index !== 0) {
+ setIndex(0);
+ return true; // Prevent default back behavior
+ }
+ // If on dashboard, allow default behavior (exit app)
+ return false;
+ };
+
+ const cleanup = registerBackButtonHandler(backHandler);
+ return cleanup;
+ }, [index]);
+
// Define navigation routes
const [routes] = useState([
{
@@ -177,7 +202,7 @@ const AppContent: React.FC = () => {
onNavigateToCompose={() => setIndex(1)}
onNavigateToDiffs={() => setIndex(2)}
onRefresh={async () => {
- await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ await triggerHapticFeedback('light');
}}
/>
@@ -247,16 +272,18 @@ const AppContent: React.FC = () => {
};
return (
-
-
-
-
+
+
+
+
+
+
);
};
diff --git a/packages/mobile-client/metro.config.js b/packages/mobile-client/metro.config.js
index a11779f..0a13a92 100644
--- a/packages/mobile-client/metro.config.js
+++ b/packages/mobile-client/metro.config.js
@@ -18,4 +18,18 @@ config.resolver.nodeModulesPaths = [
// 3. Force Metro to resolve (sub)dependencies only from the `nodeModulesPaths`
config.resolver.disableHierarchicalLookup = true;
+// 4. Disable HMR for web to avoid errors
+config.server = {
+ ...config.server,
+ enhanceMiddleware: (middleware) => {
+ return (req, res, next) => {
+ // Disable HMR for web platform
+ if (req.url && req.url.includes('platform=web')) {
+ req.url = req.url.replace(/&hot=true/g, '&hot=false');
+ }
+ return middleware(req, res, next);
+ };
+ },
+};
+
module.exports = config;
diff --git a/packages/mobile-client/src/components/AppLoading.tsx b/packages/mobile-client/src/components/AppLoading.tsx
index 671d052..a0d6d59 100644
--- a/packages/mobile-client/src/components/AppLoading.tsx
+++ b/packages/mobile-client/src/components/AppLoading.tsx
@@ -3,11 +3,12 @@
*
* Displays a loading screen while the app initializes (e.g., loading fonts).
*
- * Requirements: 2.5
+ * Requirements: 2.5, 26.8, 26.9
*/
import React from 'react';
import { View, ActivityIndicator, StyleSheet, Text } from 'react-native';
+import { getActivityIndicatorSize } from '../utils/platformAdaptations';
export interface AppLoadingProps {
message?: string;
@@ -18,11 +19,15 @@ export interface AppLoadingProps {
*
* Shows a centered activity indicator with optional message
* while the app is initializing.
+ *
+ * Uses platform-specific activity indicator styles:
+ * - iOS: UIActivityIndicatorView style
+ * - Android: Material Design CircularProgressIndicator style
*/
export const AppLoading: React.FC = ({ message = 'Loading...' }) => {
return (
-
+
{message}
);
diff --git a/packages/mobile-client/src/components/Dashboard.tsx b/packages/mobile-client/src/components/Dashboard.tsx
index 2083af6..e09fdbd 100644
--- a/packages/mobile-client/src/components/Dashboard.tsx
+++ b/packages/mobile-client/src/components/Dashboard.tsx
@@ -1,5 +1,5 @@
-import React, { useState, useEffect } from 'react';
-import { View, StyleSheet, ScrollView, FlatList, Dimensions, RefreshControl } from 'react-native';
+import React, { useState } from 'react';
+import { View, StyleSheet, ScrollView, FlatList, RefreshControl } from 'react-native';
import { useDesignSystem } from '../design-system';
import { Text } from '../design-system/typography/Text';
import { Card } from '../design-system/components/Card';
@@ -9,6 +9,7 @@ import { ProgressBar } from '../design-system/components/ProgressBar';
import { StatusIndicator, ConnectionStatus } from '../design-system/components/StatusIndicator';
import { TopAppBar } from '../navigation/TopAppBar';
import { useLoadingAnnouncement } from '../hooks/useScreenReaderAnnouncement';
+import { useResponsiveLayout } from '../hooks/useResponsiveLayout';
/**
* System metrics interface
@@ -79,25 +80,16 @@ export const Dashboard: React.FC = ({
onRefresh,
}) => {
const { theme } = useDesignSystem();
- const [isLargeScreen, setIsLargeScreen] = useState(false);
const [refreshing, setRefreshing] = useState(false);
+ // Get responsive layout configuration
+ // Requirements 13.1, 13.2, 13.5, 13.6, 13.9
+ const layout = useResponsiveLayout();
+
// Announce loading state for accessibility
// Requirement 14.11: Announce loading states
useLoadingAnnouncement(refreshing, 'Refreshing dashboard data', 'Dashboard refreshed');
- // Detect screen size for responsive layout (Requirement 13.5)
- useEffect(() => {
- const updateLayout = () => {
- const { width } = Dimensions.get('window');
- setIsLargeScreen(width >= 768);
- };
-
- updateLayout();
- const subscription = Dimensions.addEventListener('change', updateLayout);
- return () => subscription?.remove();
- }, []);
-
/**
* Handle refresh (Requirement 3.1)
*/
@@ -239,12 +231,20 @@ export const Dashboard: React.FC = ({
{/* Bento Grid Layout (Requirement 3.8, 13.6) */}
-
+
{/* Large card: Uptime (Requirement 3.3) */}
Uptime
diff --git a/packages/mobile-client/src/components/PromptComposer.tsx b/packages/mobile-client/src/components/PromptComposer.tsx
index 890b9cf..29de409 100644
--- a/packages/mobile-client/src/components/PromptComposer.tsx
+++ b/packages/mobile-client/src/components/PromptComposer.tsx
@@ -7,7 +7,8 @@ import {
TouchableOpacity,
Animated,
KeyboardAvoidingView,
- Platform,
+ Keyboard,
+ TouchableWithoutFeedback,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import * as Haptics from 'expo-haptics';
@@ -18,6 +19,7 @@ import { TopAppBar } from '../navigation/TopAppBar';
import { useDraftPrompt } from '../hooks/useDraftPrompt';
import { usePromptHistory } from '../hooks/usePromptHistory';
import { useLoadingAnnouncement } from '../hooks/useScreenReaderAnnouncement';
+import { getKeyboardBehavior, getKeyboardVerticalOffset } from '../utils/platformAdaptations';
/**
* Prompt template definition
@@ -200,273 +202,304 @@ export const PromptComposer: React.FC = ({
const isAtLimit = charCount >= MAX_CHARS;
const canSubmit = prompt.trim().length > 0 && !isLoading && !isAtLimit;
+ // Dismiss keyboard when tapping outside
+ // Requirement 25.3: Dismiss keyboard when user taps outside input field
+ const dismissKeyboard = () => {
+ Keyboard.dismiss();
+ };
+
return (
- {/* Requirement 5.1: TopAppBar */}
-
+
+
+ {/* Requirement 5.1: TopAppBar */}
+
-
- {/* Requirement 5.1, 5.3: Active Context header with "Compose Prompt" title */}
-
-
- Active Context
-
-
- Compose Prompt
-
-
-
- {/* Requirement 5.2, 5.3: Horizontal scrolling template chips container */}
-
- {templates.map((template) => (
- handleSelectTemplate(template)}
- activeOpacity={0.7}
- accessible={true}
- accessibilityLabel={`${template.label} template`}
- accessibilityHint="Double tap to insert template into prompt"
- accessibilityRole="button"
- >
-
-
- {template.label}
-
-
- ))}
-
-
- {/* Requirement 5.3: Main composer container with terminal-like header (colored dots) */}
-
- {/* Terminal-like header with colored dots */}
-
-
-
-
-
+ {/* Requirement 5.1, 5.3: Active Context header with "Compose Prompt" title */}
+
- New Instruction
+ Active Context
+
+
+ Compose Prompt
-
-
-
-
- {/* Requirement 5.4, 5.9: Multiline textarea with surfaceContainerLowest background */}
- {/* Requirement 25.1, 25.8: Keyboard handling to keep textarea visible */}
-
-
-
- {/* Requirement 5.6: Bottom toolbar with Clear and Attach buttons */}
- {/* Requirement 5.5, 5.13: Character counter with error state when limit exceeded */}
-
-
+ {templates.map((template) => (
handleSelectTemplate(template)}
activeOpacity={0.7}
accessible={true}
- accessibilityLabel="Clear prompt"
- accessibilityHint="Double tap to clear all prompt text"
+ accessibilityLabel={`${template.label} template`}
+ accessibilityHint="Double tap to insert template into prompt"
accessibilityRole="button"
- accessibilityState={{ disabled: prompt.length === 0 }}
>
-
+
+
+ {template.label}
+
+
+ ))}
+
+
+ {/* Requirement 5.3: Main composer container with terminal-like header (colored dots) */}
+
+ {/* Terminal-like header with colored dots */}
+
+
+
+
+
- Clear
+ New Instruction
-
+
-
-
- Attach
-
+
+
+ {/* Requirement 5.4, 5.9: Multiline textarea with surfaceContainerLowest background */}
+ {/* Requirement 25.1, 25.8: Keyboard handling to keep textarea visible */}
-
+
+ {/* Requirement 5.6: Bottom toolbar with Clear and Attach buttons */}
+ {/* Requirement 5.5, 5.13: Character counter with error state when limit exceeded */}
+
- {charCount} / {MAX_CHARS}
+
+
+
+
+ Clear
+
+
+
+
+
+ Attach
+
+
+
+
+
+ {charCount} / {MAX_CHARS}
+
+
+
+
+
+
+ {/* Requirement 5.8: Pro tip hint section with lightbulb icon */}
+
+
+
+
+
+ Pro Tip:{' '}
+
+ Mention specific functions or file names to help the AI understand the scope of
+ your requested changes more accurately.
-
-
- {/* Requirement 5.8: Pro tip hint section with lightbulb icon */}
-
-
-
-
-
- Pro Tip:{' '}
-
- Mention specific functions or file names to help the AI understand the scope of your
- requested changes more accurately.
-
-
-
+ {/* Error display */}
+ {error && (
+
+
+
+ {error}
+
+
+ )}
+
- {/* Error display */}
- {error && (
-
-
-
- {error}
-
-
- )}
-
-
- {/* Requirement 5.7, 5.10: Floating action button (FAB) with send icon */}
- {/* Requirement 12.6: Gradient background (primary to primaryContainer) and scale animation */}
-
-
-
- {isLoading ? (
-
- ) : (
-
- )}
-
-
-
+
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
);
};
diff --git a/packages/mobile-client/src/components/ResponsiveContainer.tsx b/packages/mobile-client/src/components/ResponsiveContainer.tsx
new file mode 100644
index 0000000..006a5be
--- /dev/null
+++ b/packages/mobile-client/src/components/ResponsiveContainer.tsx
@@ -0,0 +1,55 @@
+/**
+ * Responsive Container Component
+ *
+ * Wraps content with responsive width constraints and centering.
+ *
+ * Requirements:
+ * - 13.3: Use maximum content width of 1024px on large screens
+ * - 13.4: Center content horizontally on wide screens
+ */
+
+import React, { type ReactNode } from 'react';
+import { View, StyleSheet } from 'react-native';
+import { useResponsiveLayout } from '../hooks/useResponsiveLayout';
+
+/**
+ * Props for ResponsiveContainer
+ */
+export interface ResponsiveContainerProps {
+ children: ReactNode;
+ style?: string | undefined;
+}
+
+/**
+ * Container that constrains content width and centers on large screens
+ *
+ * Automatically applies:
+ * - Maximum width of 1024px on screens wider than that
+ * - Horizontal centering when content is constrained
+ * - Full width on smaller screens
+ */
+export const ResponsiveContainer: React.FC = ({ children, style }) => {
+ const layout = useResponsiveLayout();
+
+ return (
+
+ {children}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+});
diff --git a/packages/mobile-client/src/components/Settings.tsx b/packages/mobile-client/src/components/Settings.tsx
index ed8d170..4910e23 100644
--- a/packages/mobile-client/src/components/Settings.tsx
+++ b/packages/mobile-client/src/components/Settings.tsx
@@ -14,7 +14,8 @@ import {
ScrollView,
StyleSheet,
KeyboardAvoidingView,
- Platform,
+ Keyboard,
+ TouchableWithoutFeedback,
Linking,
TouchableOpacity,
} from 'react-native';
@@ -28,6 +29,7 @@ import { Icon } from '../design-system/components/Icon';
import { TopAppBar } from '../navigation/TopAppBar';
import { useConnection } from '../hooks/useConnection';
import { useConnectionQuality } from '../hooks/useConnectionQuality';
+import { getKeyboardBehavior, getKeyboardVerticalOffset } from '../utils/platformAdaptations';
// AsyncStorage keys
const STORAGE_KEYS = {
@@ -239,292 +241,297 @@ export const Settings: React.FC = () => {
-
- {/* Header */}
-
-
- Settings
-
- Keyboard.dismiss()}>
+
+
- Configure your development environment
-
-
-
- {/* Connectivity Bento Cards */}
-
-
- {/* Status Card */}
-
-
- Status
+ {/* Header */}
+
+
+ Settings
- {getStatusLabel()}
-
-
-
- {/* Latency Card */}
-
-
- Latency
+ Configure your development environment
+
+
+ {/* Connectivity Bento Cards */}
+
+
+ {/* Status Card */}
+
+
+ Status
+
+
+ {getStatusLabel()}
+
+
+
+ {/* Latency Card */}
+
+
+ Latency
+
+
+ {latency !== null ? `${latency}ms` : '--'}
+
+
+
+ {/* Active Instance Card */}
+
+
+ Active Instance
+
+
+ localhost:8080
+
+
+
+ {/* Load Card */}
+
+
+ Quality
+
+
+ {getQualityLabel()}
+
+
+
+
+
+ {/* Infrastructure Section */}
+
- {latency !== null ? `${latency}ms` : '--'}
-
-
-
- {/* Active Instance Card */}
-
-
- Active Instance
+ Infrastructure
+
+
+
+
+
+ {/* Appearance Section */}
+
- localhost:8080
-
-
-
- {/* Load Card */}
-
-
- Quality
+ Appearance
+
+
+
+
+
+
+
+ {/* Communication Section */}
+
- {getQualityLabel()}
+ Communication
-
-
-
-
- {/* Infrastructure Section */}
-
-
- Infrastructure
-
-
-
-
-
-
- {/* Appearance Section */}
-
-
- Appearance
-
-
-
-
-
-
-
-
- {/* Communication Section */}
-
-
- Communication
-
-
-
-
-
-
-
-
- {/* About Section */}
-
-
- About
-
-
- {/* App Icon and Version */}
-
-
-
-
-
-
- CodeLink
-
-
- Version {APP_VERSION}
-
-
+
+
+
+
+
- {/* Links */}
-
- openLink('https://github.com/codelink/docs')}
- accessible={true}
- accessibilityLabel="Open documentation"
- accessibilityHint="Opens documentation in browser"
- accessibilityRole="link"
- >
-
-
- Documentation
-
-
-
- openLink('https://github.com/codelink/support')}
- accessible={true}
- accessibilityLabel="Open support"
- accessibilityHint="Opens support page in browser"
- accessibilityRole="link"
- >
-
-
- Support
-
-
-
- openLink('https://github.com/codelink')}
- accessible={true}
- accessibilityLabel="Open GitHub repository"
- accessibilityHint="Opens GitHub repository in browser"
- accessibilityRole="link"
+ {/* About Section */}
+
+
-
-
- GitHub
-
-
+ About
+
+
+ {/* App Icon and Version */}
+
+
+
+
+
+
+ CodeLink
+
+
+ Version {APP_VERSION}
+
+
+
+
+ {/* Links */}
+
+ openLink('https://github.com/codelink/docs')}
+ accessible={true}
+ accessibilityLabel="Open documentation"
+ accessibilityHint="Opens documentation in browser"
+ accessibilityRole="link"
+ >
+
+
+ Documentation
+
+
+
+ openLink('https://github.com/codelink/support')}
+ accessible={true}
+ accessibilityLabel="Open support"
+ accessibilityHint="Opens support page in browser"
+ accessibilityRole="link"
+ >
+
+
+ Support
+
+
+
+ openLink('https://github.com/codelink')}
+ accessible={true}
+ accessibilityLabel="Open GitHub repository"
+ accessibilityHint="Opens GitHub repository in browser"
+ accessibilityRole="link"
+ >
+
+
+ GitHub
+
+
+
+
-
-
- {/* Bottom padding for safe area */}
-
-
+ {/* Bottom padding for safe area */}
+
+
+
+
);
diff --git a/packages/mobile-client/src/components/index.ts b/packages/mobile-client/src/components/index.ts
index 2f9dd97..15b6b88 100644
--- a/packages/mobile-client/src/components/index.ts
+++ b/packages/mobile-client/src/components/index.ts
@@ -17,3 +17,5 @@ export { PromptTemplates } from './PromptTemplates';
export type { PromptTemplate, PromptTemplatesProps } from './PromptTemplates';
export { AppLoading } from './AppLoading';
export type { AppLoadingProps } from './AppLoading';
+export { ResponsiveContainer } from './ResponsiveContainer';
+export type { ResponsiveContainerProps } from './ResponsiveContainer';
diff --git a/packages/mobile-client/src/design-system/components/Button.tsx b/packages/mobile-client/src/design-system/components/Button.tsx
index e1ff12a..e1dc58c 100644
--- a/packages/mobile-client/src/design-system/components/Button.tsx
+++ b/packages/mobile-client/src/design-system/components/Button.tsx
@@ -17,11 +17,10 @@ import {
ActivityIndicator,
StyleProp,
ViewStyle,
- Platform,
} from 'react-native';
-import * as Haptics from 'expo-haptics';
import { LinearGradient } from 'expo-linear-gradient';
import { useDesignSystem } from '../theme/useDesignSystem';
+import { triggerHapticFeedback } from '../../utils/platformAdaptations';
/**
* Button variant types
@@ -180,24 +179,13 @@ export const Button: React.FC = ({
/**
* Handle press - trigger haptic feedback and callback
+ * Requirements: 22.1, 22.6, 22.7
*/
- const handlePress = () => {
+ const handlePress = async () => {
if (!isInteractive) return;
- // Trigger haptic feedback
- if (Platform.OS === 'ios' || Platform.OS === 'android') {
- switch (hapticFeedback) {
- case 'light':
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
- break;
- case 'medium':
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
- break;
- case 'heavy':
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);
- break;
- }
- }
+ // Trigger platform-specific haptic feedback
+ await triggerHapticFeedback(hapticFeedback);
onPress();
};
diff --git a/packages/mobile-client/src/design-system/components/GlassContainer.tsx b/packages/mobile-client/src/design-system/components/GlassContainer.tsx
new file mode 100644
index 0000000..289ba73
--- /dev/null
+++ b/packages/mobile-client/src/design-system/components/GlassContainer.tsx
@@ -0,0 +1,131 @@
+/**
+ * Glass Container Component
+ *
+ * A container with glassmorphism effect using backdrop blur.
+ * Provides a frosted glass visual effect for floating elements.
+ *
+ * Requirements:
+ * - 11.1: Apply backdrop-blur effect to Bottom_Navigation bar
+ * - 11.2: Apply backdrop-blur effect to modal overlays
+ * - 11.3: Apply backdrop-blur effect to floating action buttons
+ * - 11.4: Use 20px blur radius for glassmorphism effects
+ * - 11.5: Use 80-90% opacity for glass surfaces
+ * - 11.6: Layer glass effects over surface-variant background
+ * - 11.7: Fallback to solid background when backdrop-blur not supported
+ */
+
+import React, { type ReactNode } from 'react';
+import { View, StyleSheet, Platform, StyleProp, ViewStyle } from 'react-native';
+import { BlurView } from 'expo-blur';
+import { useDesignSystem } from '../theme/useDesignSystem';
+
+/**
+ * Glass container props
+ */
+export interface GlassContainerProps {
+ /**
+ * Child components to render inside the glass container
+ */
+ children: ReactNode;
+
+ /**
+ * Blur intensity (0-100)
+ * @default 80
+ */
+ intensity?: number;
+
+ /**
+ * Tint color for the blur
+ * @default 'dark'
+ */
+ tint?: 'light' | 'dark' | 'default';
+
+ /**
+ * Background opacity (0-1)
+ * @default 0.8
+ */
+ opacity?: number;
+
+ /**
+ * Custom style overrides
+ */
+ style?: StyleProp;
+
+ /**
+ * Whether to use fallback solid background on unsupported platforms
+ * @default true
+ */
+ useFallback?: boolean;
+}
+
+/**
+ * Glass container component with glassmorphism effect
+ *
+ * Uses BlurView for backdrop blur on supported platforms (iOS, Android).
+ * Falls back to semi-transparent solid background on web or when blur is not supported.
+ */
+export const GlassContainer: React.FC = ({
+ children,
+ intensity = 80,
+ tint = 'dark',
+ opacity = 0.8,
+ style,
+ useFallback = true,
+}) => {
+ const { theme } = useDesignSystem();
+
+ // Check if platform supports blur
+ const supportsBlur = Platform.OS === 'ios' || Platform.OS === 'android';
+
+ // Fallback background color with opacity
+ const fallbackBackgroundColor = `${theme.colors.surfaceVariant}${Math.round(opacity * 255)
+ .toString(16)
+ .padStart(2, '0')}`;
+
+ if (!supportsBlur && useFallback) {
+ // Fallback for platforms without blur support (Requirement 11.7)
+ return (
+
+ {children}
+
+ );
+ }
+
+ // Glass effect with BlurView (Requirements 11.4, 11.5, 11.6)
+ return (
+
+ {/* Backdrop blur layer */}
+
+
+ {/* Semi-transparent background overlay */}
+
+
+ {/* Content layer */}
+ {children}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ overflow: 'hidden',
+ },
+ content: {
+ flex: 1,
+ },
+});
diff --git a/packages/mobile-client/src/design-system/components/Toggle.tsx b/packages/mobile-client/src/design-system/components/Toggle.tsx
index 05196c8..c988019 100644
--- a/packages/mobile-client/src/design-system/components/Toggle.tsx
+++ b/packages/mobile-client/src/design-system/components/Toggle.tsx
@@ -8,9 +8,9 @@
*/
import React from 'react';
-import { View, Switch, Text, StyleSheet, Platform, StyleProp, ViewStyle } from 'react-native';
-import * as Haptics from 'expo-haptics';
+import { View, Switch, Text, StyleSheet, StyleProp, ViewStyle } from 'react-native';
import { useDesignSystem } from '../theme/useDesignSystem';
+import { triggerHapticFeedback } from '../../utils/platformAdaptations';
export interface ToggleProps {
/**
@@ -81,13 +81,14 @@ export const Toggle: React.FC = ({
/**
* Handle value change with haptic feedback
+ * Requirements: 22.5, 22.6, 22.7
*/
- const handleValueChange = (newValue: boolean) => {
+ const handleValueChange = async (newValue: boolean) => {
if (disabled) return;
- // Trigger haptic feedback
- if (hapticFeedback && (Platform.OS === 'ios' || Platform.OS === 'android')) {
- Haptics.selectionAsync();
+ // Trigger platform-specific haptic feedback
+ if (hapticFeedback) {
+ await triggerHapticFeedback('selection');
}
onValueChange(newValue);
diff --git a/packages/mobile-client/src/design-system/components/index.ts b/packages/mobile-client/src/design-system/components/index.ts
index 149e745..b24343e 100644
--- a/packages/mobile-client/src/design-system/components/index.ts
+++ b/packages/mobile-client/src/design-system/components/index.ts
@@ -28,3 +28,5 @@ export { Skeleton } from './Skeleton';
export type { SkeletonProps } from './Skeleton';
export { ToastContainer, showToast } from './Toast';
export type { ToastMessage, ToastVariant } from './Toast';
+export { GlassContainer } from './GlassContainer';
+export type { GlassContainerProps } from './GlassContainer';
diff --git a/packages/mobile-client/src/hooks/index.ts b/packages/mobile-client/src/hooks/index.ts
index 6fb49fc..a244e0b 100644
--- a/packages/mobile-client/src/hooks/index.ts
+++ b/packages/mobile-client/src/hooks/index.ts
@@ -28,3 +28,7 @@ export {
useScreenChangeAnnouncement,
useLoadingAnnouncement,
} from './useScreenReaderAnnouncement';
+
+export { usePlatformNavigation } from './usePlatformNavigation';
+
+export { useResponsiveLayout, type ResponsiveLayoutConfig } from './useResponsiveLayout';
diff --git a/packages/mobile-client/src/hooks/usePlatformNavigation.tsx b/packages/mobile-client/src/hooks/usePlatformNavigation.tsx
new file mode 100644
index 0000000..93e6a3f
--- /dev/null
+++ b/packages/mobile-client/src/hooks/usePlatformNavigation.tsx
@@ -0,0 +1,39 @@
+/**
+ * Platform-specific navigation configuration hook
+ *
+ * Requirements:
+ * - 26.5: Platform-specific navigation gestures
+ * - 26.6: iOS swipe-back gesture support
+ * - 26.7: Android hardware back button support
+ */
+
+import { useMemo } from 'react';
+import type { StackNavigationOptions } from '@react-navigation/stack';
+import { supportsSwipeBack, getNavigationGestureConfig } from '../utils/platformAdaptations';
+
+/**
+ * Hook to get platform-specific navigation options
+ *
+ * @returns Navigation options configured for the current platform
+ */
+export const usePlatformNavigation = (): Partial => {
+ const navigationOptions = useMemo(() => {
+ const gestureConfig = getNavigationGestureConfig();
+
+ return {
+ // Requirement 26.6: iOS swipe-back gesture support
+ gestureEnabled: supportsSwipeBack() ? gestureConfig.gestureEnabled : false,
+ gestureDirection: gestureConfig.gestureDirection,
+
+ // Screen transition animations (Requirements 12.1, 12.2)
+ animation: 'slide_from_right' as const,
+ animationDuration: 300,
+ animationTypeForReplace: 'push' as const,
+
+ // Header configuration
+ headerShown: false, // We use custom TopAppBar
+ };
+ }, []);
+
+ return navigationOptions;
+};
diff --git a/packages/mobile-client/src/hooks/useResponsiveLayout.tsx b/packages/mobile-client/src/hooks/useResponsiveLayout.tsx
new file mode 100644
index 0000000..9333bc6
--- /dev/null
+++ b/packages/mobile-client/src/hooks/useResponsiveLayout.tsx
@@ -0,0 +1,131 @@
+/**
+ * Responsive Layout Hook
+ *
+ * Combines orientation detection with responsive layout utilities
+ * to provide a complete responsive layout solution.
+ *
+ * Requirements:
+ * - 13.1: Support portrait orientation on all screens
+ * - 13.2: Support landscape orientation on all screens
+ * - 13.3: Use maximum content width of 1024px on large screens
+ * - 13.4: Center content horizontally on wide screens
+ * - 13.5: Use responsive grid layouts
+ * - 13.6: Adjust bento grid layout based on orientation
+ * - 13.7: Adjust typography scales based on screen size
+ * - 13.9: Re-layout content smoothly on orientation change
+ */
+
+import { useMemo } from 'react';
+import { useOrientation } from './useOrientation';
+import {
+ getScreenSize,
+ getGridColumns,
+ getBentoGridConfig,
+ getContentWidth,
+ getTypographyScale,
+ getResponsivePadding,
+ isLargeScreen,
+ isSmallScreen,
+ type ScreenSize,
+} from '../utils/responsiveLayout';
+
+/**
+ * Responsive layout configuration
+ */
+export interface ResponsiveLayoutConfig {
+ // Screen information
+ screenSize: ScreenSize;
+ isLargeScreen: boolean;
+ isSmallScreen: boolean;
+
+ // Orientation
+ orientation: 'portrait' | 'landscape';
+ isPortrait: boolean;
+ isLandscape: boolean;
+
+ // Dimensions
+ width: number;
+ height: number;
+
+ // Content width
+ contentWidth: number;
+ shouldCenterContent: boolean;
+ contentMarginHorizontal: number;
+
+ // Grid configuration
+ gridColumns: number;
+
+ // Bento grid configuration
+ bentoGrid: {
+ columns: number;
+ largeCardSpan: number;
+ smallCardSpan: number;
+ gap: number;
+ padding: number;
+ };
+
+ // Typography scale
+ typographyScale: number;
+
+ // Padding
+ padding: {
+ horizontal: number;
+ vertical: number;
+ };
+}
+
+/**
+ * Hook to get responsive layout configuration
+ *
+ * Automatically updates when screen size or orientation changes.
+ * Requirement 13.9: Re-layout content smoothly on orientation change
+ *
+ * @returns Responsive layout configuration
+ */
+export const useResponsiveLayout = (): ResponsiveLayoutConfig => {
+ const { orientation, isPortrait, isLandscape, width, height } = useOrientation();
+
+ const config = useMemo(() => {
+ const screenSize = getScreenSize(width);
+ const contentWidthConfig = getContentWidth(width);
+ const gridColumns = getGridColumns(screenSize, orientation);
+ const bentoGrid = getBentoGridConfig(screenSize, orientation);
+ const typographyScale = getTypographyScale(screenSize);
+ const padding = getResponsivePadding(screenSize);
+
+ return {
+ // Screen information
+ screenSize,
+ isLargeScreen: isLargeScreen(width),
+ isSmallScreen: isSmallScreen(width),
+
+ // Orientation
+ orientation,
+ isPortrait,
+ isLandscape,
+
+ // Dimensions
+ width,
+ height,
+
+ // Content width
+ contentWidth: contentWidthConfig.width,
+ shouldCenterContent: contentWidthConfig.shouldCenter,
+ contentMarginHorizontal: contentWidthConfig.marginHorizontal,
+
+ // Grid configuration
+ gridColumns,
+
+ // Bento grid configuration
+ bentoGrid,
+
+ // Typography scale
+ typographyScale,
+
+ // Padding
+ padding,
+ };
+ }, [orientation, isPortrait, isLandscape, width, height]);
+
+ return config;
+};
diff --git a/packages/mobile-client/src/navigation/BottomNavBar.tsx b/packages/mobile-client/src/navigation/BottomNavBar.tsx
index fe6e9ec..b1b8523 100644
--- a/packages/mobile-client/src/navigation/BottomNavBar.tsx
+++ b/packages/mobile-client/src/navigation/BottomNavBar.tsx
@@ -10,6 +10,7 @@
import React, { useEffect, useRef } from 'react';
import { View, TouchableOpacity, StyleSheet, Platform, Animated } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import { BlurView } from 'expo-blur';
import * as Haptics from 'expo-haptics';
import { useDesignSystem } from '../design-system';
import { Text } from '../design-system/typography/Text';
@@ -168,17 +169,8 @@ export const BottomNavBar: React.FC = ({ activeRoute, onNavig
},
]}
>
- {/*
- Glassmorphism backdrop with expo-blur
-
- To enable:
- 1. Install: npm install expo-blur --legacy-peer-deps
- 2. Uncomment the import at the top of this file
- 3. Uncomment the BlurView below
-
- See INSTALL_EXPO_BLUR.md for details
- */}
- {/* */}
+ {/* Glassmorphism backdrop with expo-blur (Requirements 11.1, 11.4, 11.5) */}
+
{NAV_ITEMS.map((item) => {
diff --git a/packages/mobile-client/src/utils/index.ts b/packages/mobile-client/src/utils/index.ts
index 59bb83e..becf0c7 100644
--- a/packages/mobile-client/src/utils/index.ts
+++ b/packages/mobile-client/src/utils/index.ts
@@ -28,3 +28,44 @@ export {
handleError,
handleUnknownError,
} from './errorHandling';
+
+// Export platform adaptation utilities
+export {
+ type PlatformType,
+ getCurrentPlatform,
+ isIOS,
+ isAndroid,
+ isWeb,
+ getStatusBarStyle,
+ getActivityIndicatorSize,
+ triggerHapticFeedback,
+ registerBackButtonHandler,
+ getKeyboardBehavior,
+ getKeyboardVerticalOffset,
+ supportsSwipeBack,
+ hasHardwareBackButton,
+ getNavigationGestureConfig,
+} from './platformAdaptations';
+
+// Export responsive layout utilities
+export {
+ BREAKPOINTS,
+ MAX_CONTENT_WIDTH,
+ MIN_TOUCH_TARGET_SIZE,
+ type ScreenSize,
+ getScreenDimensions,
+ getScreenSize,
+ getGridColumns,
+ getBentoGridConfig,
+ getContentWidth,
+ getTypographyScale,
+ scaleFont,
+ scaleSpacing,
+ ensureMinTouchTarget,
+ getResponsivePadding,
+ isLargeScreen,
+ isSmallScreen,
+ getPixelRatio,
+ dpToPixels,
+ pixelsToDp,
+} from './responsiveLayout';
diff --git a/packages/mobile-client/src/utils/platformAdaptations.ts b/packages/mobile-client/src/utils/platformAdaptations.ts
new file mode 100644
index 0000000..1c5259b
--- /dev/null
+++ b/packages/mobile-client/src/utils/platformAdaptations.ts
@@ -0,0 +1,215 @@
+/**
+ * Platform-specific adaptations for iOS and Android
+ *
+ * Requirements:
+ * - 26.1: Platform-specific status bar styling
+ * - 26.2: Platform-specific safe area insets
+ * - 26.3: Platform-specific keyboard behavior
+ * - 26.4: Platform-specific haptic feedback patterns
+ * - 26.5: Platform-specific navigation gestures
+ * - 26.6: iOS swipe-back gesture support
+ * - 26.7: Android hardware back button support
+ * - 26.8: iOS-style activity indicator
+ * - 26.9: Android Material Design activity indicator
+ */
+
+import { Platform, BackHandler } from 'react-native';
+import * as Haptics from 'expo-haptics';
+
+/**
+ * Platform type
+ */
+export type PlatformType = 'ios' | 'android' | 'web' | 'other';
+
+/**
+ * Get current platform
+ */
+export const getCurrentPlatform = (): PlatformType => {
+ if (Platform.OS === 'ios') return 'ios';
+ if (Platform.OS === 'android') return 'android';
+ if (Platform.OS === 'web') return 'web';
+ return 'other';
+};
+
+/**
+ * Check if running on iOS
+ */
+export const isIOS = (): boolean => Platform.OS === 'ios';
+
+/**
+ * Check if running on Android
+ */
+export const isAndroid = (): boolean => Platform.OS === 'android';
+
+/**
+ * Check if running on web
+ */
+export const isWeb = (): boolean => Platform.OS === 'web';
+
+/**
+ * Get platform-specific status bar style
+ * Requirement 26.1: Platform-specific status bar styling
+ */
+export const getStatusBarStyle = (isDark: boolean): 'light' | 'dark' | 'auto' => {
+ if (isIOS()) {
+ // iOS uses light content for dark backgrounds, dark content for light backgrounds
+ return isDark ? 'light' : 'dark';
+ }
+
+ if (isAndroid()) {
+ // Android also follows the same pattern
+ return isDark ? 'light' : 'dark';
+ }
+
+ return 'auto';
+};
+
+/**
+ * Get platform-specific activity indicator size
+ * Requirements 26.8, 26.9: Platform-specific activity indicators
+ */
+export const getActivityIndicatorSize = (): 'small' | 'large' => {
+ // Both platforms use the same sizes, but rendering differs
+ return 'large';
+};
+
+/**
+ * Haptic feedback patterns for different platforms
+ * Requirement 26.4: Platform-specific haptic feedback patterns
+ */
+export const triggerHapticFeedback = async (
+ type: 'light' | 'medium' | 'heavy' | 'success' | 'error' | 'selection'
+): Promise => {
+ try {
+ if (isIOS()) {
+ // iOS haptic patterns
+ switch (type) {
+ case 'light':
+ await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ break;
+ case 'medium':
+ await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
+ break;
+ case 'heavy':
+ await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);
+ break;
+ case 'success':
+ await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
+ break;
+ case 'error':
+ await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
+ break;
+ case 'selection':
+ await Haptics.selectionAsync();
+ break;
+ }
+ } else if (isAndroid()) {
+ // Android haptic patterns (more subtle)
+ switch (type) {
+ case 'light':
+ case 'selection':
+ await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ break;
+ case 'medium':
+ await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
+ break;
+ case 'heavy':
+ case 'success':
+ case 'error':
+ await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);
+ break;
+ }
+ }
+ } catch (error) {
+ // Gracefully handle haptic feedback errors
+ console.warn('Haptic feedback not available:', error);
+ }
+};
+
+/**
+ * Register Android hardware back button handler
+ * Requirement 26.7: Android hardware back button support
+ *
+ * @param handler Function to call when back button is pressed
+ * @returns Cleanup function to remove the handler
+ */
+export const registerBackButtonHandler = (handler: () => boolean): (() => void) => {
+ if (isAndroid()) {
+ const subscription = BackHandler.addEventListener('hardwareBackPress', handler);
+ return () => subscription.remove();
+ }
+
+ // No-op for other platforms
+ return () => {};
+};
+
+/**
+ * Get platform-specific keyboard behavior settings
+ * Requirement 26.3: Platform-specific keyboard behavior
+ */
+export const getKeyboardBehavior = (): 'padding' | 'height' | 'position' | undefined => {
+ if (isIOS()) {
+ return 'padding';
+ }
+
+ if (isAndroid()) {
+ return 'height';
+ }
+
+ return undefined;
+};
+
+/**
+ * Get platform-specific keyboard vertical offset
+ * Requirement 26.3: Platform-specific keyboard behavior
+ */
+export const getKeyboardVerticalOffset = (hasHeader: boolean = true): number => {
+ if (isIOS()) {
+ // iOS needs offset for header height
+ return hasHeader ? 64 : 0;
+ }
+
+ // Android doesn't need offset
+ return 0;
+};
+
+/**
+ * Check if platform supports swipe-back gesture
+ * Requirement 26.6: iOS swipe-back gesture support
+ */
+export const supportsSwipeBack = (): boolean => {
+ return isIOS();
+};
+
+/**
+ * Check if platform has hardware back button
+ * Requirement 26.7: Android hardware back button support
+ */
+export const hasHardwareBackButton = (): boolean => {
+ return isAndroid();
+};
+
+/**
+ * Get platform-specific navigation gesture config
+ * Requirement 26.5: Platform-specific navigation gestures
+ */
+export const getNavigationGestureConfig = () => {
+ if (isIOS()) {
+ return {
+ gestureEnabled: true,
+ gestureDirection: 'horizontal' as const,
+ };
+ }
+
+ if (isAndroid()) {
+ return {
+ gestureEnabled: false, // Android uses hardware back button
+ gestureDirection: 'horizontal' as const,
+ };
+ }
+
+ return {
+ gestureEnabled: false,
+ gestureDirection: 'horizontal' as const,
+ };
+};
diff --git a/packages/mobile-client/src/utils/responsiveLayout.ts b/packages/mobile-client/src/utils/responsiveLayout.ts
new file mode 100644
index 0000000..d332857
--- /dev/null
+++ b/packages/mobile-client/src/utils/responsiveLayout.ts
@@ -0,0 +1,253 @@
+/**
+ * Responsive Layout Utilities
+ *
+ * Provides utilities for responsive layouts that adapt to different
+ * screen sizes and orientations.
+ *
+ * Requirements:
+ * - 13.1: Support portrait orientation on all screens
+ * - 13.2: Support landscape orientation on all screens
+ * - 13.3: Use maximum content width of 1024px on large screens
+ * - 13.4: Center content horizontally on wide screens
+ * - 13.5: Use responsive grid layouts (1 column on small, 2-3 on medium/large)
+ * - 13.6: Adjust bento grid layout based on orientation
+ * - 13.7: Adjust typography scales based on screen size
+ * - 13.8: Maintain minimum touch target size of 44x44pt on all screen sizes
+ * - 13.9: Re-layout content smoothly on orientation change
+ */
+
+import { Dimensions, PixelRatio } from 'react-native';
+import type { Orientation } from '../hooks/useOrientation';
+
+/**
+ * Screen size breakpoints (in dp/pt)
+ */
+export const BREAKPOINTS = {
+ small: 0, // 0-599dp (phones in portrait)
+ medium: 600, // 600-839dp (large phones, small tablets)
+ large: 840, // 840-1023dp (tablets)
+ xlarge: 1024, // 1024dp+ (large tablets, desktops)
+} as const;
+
+/**
+ * Maximum content width for large screens (in dp/pt)
+ * Requirement 13.3: Use maximum content width of 1024px on large screens
+ */
+export const MAX_CONTENT_WIDTH = 1024;
+
+/**
+ * Minimum touch target size (in dp/pt)
+ * Requirement 13.8: Maintain minimum touch target size of 44x44pt
+ */
+export const MIN_TOUCH_TARGET_SIZE = 44;
+
+/**
+ * Screen size category
+ */
+export type ScreenSize = 'small' | 'medium' | 'large' | 'xlarge';
+
+/**
+ * Get current screen dimensions
+ */
+export const getScreenDimensions = () => {
+ return Dimensions.get('window');
+};
+
+/**
+ * Get screen size category based on width
+ *
+ * @param width Screen width in dp/pt
+ * @returns Screen size category
+ */
+export const getScreenSize = (width: number): ScreenSize => {
+ if (width >= BREAKPOINTS.xlarge) return 'xlarge';
+ if (width >= BREAKPOINTS.large) return 'large';
+ if (width >= BREAKPOINTS.medium) return 'medium';
+ return 'small';
+};
+
+/**
+ * Get number of grid columns based on screen size
+ * Requirement 13.5: Use responsive grid layouts (1 column on small, 2-3 on medium/large)
+ *
+ * @param screenSize Screen size category
+ * @param orientation Current orientation
+ * @returns Number of columns for grid layout
+ */
+export const getGridColumns = (screenSize: ScreenSize, orientation: Orientation): number => {
+ if (screenSize === 'small') {
+ return orientation === 'portrait' ? 1 : 2;
+ }
+
+ if (screenSize === 'medium') {
+ return orientation === 'portrait' ? 2 : 3;
+ }
+
+ // large and xlarge
+ return orientation === 'portrait' ? 2 : 3;
+};
+
+/**
+ * Get bento grid configuration based on screen size and orientation
+ * Requirement 13.6: Adjust bento grid layout based on orientation
+ *
+ * @param screenSize Screen size category
+ * @param orientation Current orientation
+ * @returns Bento grid configuration
+ */
+export const getBentoGridConfig = (screenSize: ScreenSize, orientation: Orientation) => {
+ const isSmall = screenSize === 'small';
+ const isPortrait = orientation === 'portrait';
+
+ return {
+ // Use single column on small screens in portrait
+ columns: isSmall && isPortrait ? 1 : 2,
+
+ // Asymmetrical sizing for bento pattern
+ largeCardSpan: isSmall && isPortrait ? 1 : 2,
+ smallCardSpan: 1,
+
+ // Gap between cards
+ gap: isSmall ? 12 : 16,
+
+ // Padding around grid
+ padding: isSmall ? 16 : 24,
+ };
+};
+
+/**
+ * Get content container width with max width constraint
+ * Requirement 13.3: Use maximum content width of 1024px on large screens
+ * Requirement 13.4: Center content horizontally on wide screens
+ *
+ * @param screenWidth Current screen width
+ * @returns Content width and whether it should be centered
+ */
+export const getContentWidth = (screenWidth: number) => {
+ const shouldConstrain = screenWidth > MAX_CONTENT_WIDTH;
+
+ return {
+ width: shouldConstrain ? MAX_CONTENT_WIDTH : screenWidth,
+ shouldCenter: shouldConstrain,
+ marginHorizontal: shouldConstrain ? (screenWidth - MAX_CONTENT_WIDTH) / 2 : 0,
+ };
+};
+
+/**
+ * Get typography scale multiplier based on screen size
+ * Requirement 13.7: Adjust typography scales based on screen size
+ *
+ * @param screenSize Screen size category
+ * @returns Scale multiplier for font sizes
+ */
+export const getTypographyScale = (screenSize: ScreenSize): number => {
+ switch (screenSize) {
+ case 'small':
+ return 0.9; // Slightly smaller on small screens
+ case 'medium':
+ return 1.0; // Base scale
+ case 'large':
+ return 1.05; // Slightly larger on large screens
+ case 'xlarge':
+ return 1.1; // Larger on extra large screens
+ }
+};
+
+/**
+ * Scale a font size based on screen size
+ *
+ * @param baseSize Base font size
+ * @param screenSize Screen size category
+ * @returns Scaled font size
+ */
+export const scaleFont = (baseSize: number, screenSize: ScreenSize): number => {
+ return Math.round(baseSize * getTypographyScale(screenSize));
+};
+
+/**
+ * Get spacing value based on screen size
+ *
+ * @param baseSpacing Base spacing value
+ * @param screenSize Screen size category
+ * @returns Scaled spacing value
+ */
+export const scaleSpacing = (baseSpacing: number, screenSize: ScreenSize): number => {
+ const scale = screenSize === 'small' ? 0.875 : 1.0;
+ return Math.round(baseSpacing * scale);
+};
+
+/**
+ * Ensure touch target meets minimum size requirement
+ * Requirement 13.8: Maintain minimum touch target size of 44x44pt
+ *
+ * @param size Desired size
+ * @returns Size that meets minimum requirement
+ */
+export const ensureMinTouchTarget = (size: number): number => {
+ return Math.max(size, MIN_TOUCH_TARGET_SIZE);
+};
+
+/**
+ * Get responsive padding based on screen size
+ *
+ * @param screenSize Screen size category
+ * @returns Padding values for different screen sizes
+ */
+export const getResponsivePadding = (screenSize: ScreenSize) => {
+ switch (screenSize) {
+ case 'small':
+ return { horizontal: 16, vertical: 12 };
+ case 'medium':
+ return { horizontal: 24, vertical: 16 };
+ case 'large':
+ case 'xlarge':
+ return { horizontal: 32, vertical: 20 };
+ }
+};
+
+/**
+ * Check if screen is considered large
+ *
+ * @param width Screen width
+ * @returns True if screen is large or xlarge
+ */
+export const isLargeScreen = (width: number): boolean => {
+ return getScreenSize(width) === 'large' || getScreenSize(width) === 'xlarge';
+};
+
+/**
+ * Check if screen is considered small
+ *
+ * @param width Screen width
+ * @returns True if screen is small
+ */
+export const isSmallScreen = (width: number): boolean => {
+ return getScreenSize(width) === 'small';
+};
+
+/**
+ * Get pixel ratio for the current device
+ */
+export const getPixelRatio = (): number => {
+ return PixelRatio.get();
+};
+
+/**
+ * Convert dp/pt to pixels
+ *
+ * @param dp Value in dp/pt
+ * @returns Value in pixels
+ */
+export const dpToPixels = (dp: number): number => {
+ return PixelRatio.getPixelSizeForLayoutSize(dp);
+};
+
+/**
+ * Convert pixels to dp/pt
+ *
+ * @param pixels Value in pixels
+ * @returns Value in dp/pt
+ */
+export const pixelsToDp = (pixels: number): number => {
+ return pixels / PixelRatio.get();
+};
diff --git a/packages/mobile-client/webpack.config.js b/packages/mobile-client/webpack.config.js
new file mode 100644
index 0000000..91874c6
--- /dev/null
+++ b/packages/mobile-client/webpack.config.js
@@ -0,0 +1,20 @@
+const createExpoWebpackConfigAsync = require('@expo/webpack-config');
+
+module.exports = async function (env, argv) {
+ const config = await createExpoWebpackConfigAsync(
+ {
+ ...env,
+ // Disable HMR for web to avoid the error
+ mode: env.mode || 'development',
+ },
+ argv
+ );
+
+ // Disable HMR
+ if (config.devServer) {
+ config.devServer.hot = false;
+ config.devServer.liveReload = false;
+ }
+
+ return config;
+};
diff --git a/packages/relay-server/tsconfig.json b/packages/relay-server/tsconfig.json
index fd5ac11..b6e4437 100644
--- a/packages/relay-server/tsconfig.json
+++ b/packages/relay-server/tsconfig.json
@@ -7,5 +7,6 @@
"lib": ["ES2020"],
"types": ["vitest/globals", "node"]
},
- "include": ["src/**/*"]
+ "include": ["src/**/*"],
+ "references": [{ "path": "../protocol" }]
}
diff --git a/packages/vscode-extension/tsconfig.json b/packages/vscode-extension/tsconfig.json
index fd5ac11..b6e4437 100644
--- a/packages/vscode-extension/tsconfig.json
+++ b/packages/vscode-extension/tsconfig.json
@@ -7,5 +7,6 @@
"lib": ["ES2020"],
"types": ["vitest/globals", "node"]
},
- "include": ["src/**/*"]
+ "include": ["src/**/*"],
+ "references": [{ "path": "../protocol" }]
}