Implement system bars management and Edge-to-Edge layout

This commit is contained in:
2026-07-29 14:02:35 +02:00
parent a12ec6ae2f
commit 038cbc674c
8 changed files with 55 additions and 14 deletions
+10 -1
View File
@@ -1,5 +1,7 @@
import { StatusBar } from 'expo-status-bar';
import React, { useEffect } from 'react';
import { Platform } from 'react-native';
import * as NavigationBar from 'expo-navigation-bar';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { SessionProvider } from './src/auth/session';
import { getDb } from './src/db/database';
@@ -9,13 +11,20 @@ export default function App() {
// Abre y migra la BD local al arrancar.
useEffect(() => {
void getDb();
// Configuración global de la barra de navegación en Android (Edge-to-Edge)
if (Platform.OS === 'android') {
void NavigationBar.setPositionAsync('absolute');
void NavigationBar.setBackgroundColorAsync('#ffffff00'); // Transparente
void NavigationBar.setButtonStyleAsync('dark'); // Iconos oscuros por defecto
}
}, []);
return (
<SafeAreaProvider>
<SessionProvider>
<RootNavigator />
<StatusBar style="light" />
<StatusBar style="auto" />
</SessionProvider>
</SafeAreaProvider>
);
+15
View File
@@ -18,6 +18,7 @@
"expo-file-system": "~56.0.8",
"expo-image-picker": "~56.0.18",
"expo-location": "~56.0.18",
"expo-navigation-bar": "^57.0.2",
"expo-screen-orientation": "^57.0.1",
"expo-secure-store": "~56.0.4",
"expo-sqlite": "~56.0.5",
@@ -5911,6 +5912,20 @@
"react-native": "*"
}
},
"node_modules/expo-navigation-bar": {
"version": "57.0.2",
"resolved": "https://registry.npmjs.org/expo-navigation-bar/-/expo-navigation-bar-57.0.2.tgz",
"integrity": "sha512-Ufe49dsTZjarA7gMMwwwnNJWpXo+oTyl5Ud0g3lsGVrj2HXjyw9cb9098N8+CoWpjfIa5PghuRYYXrcCrGQIHg==",
"license": "MIT",
"dependencies": {
"debug": "^4.3.2"
},
"peerDependencies": {
"expo": "*",
"react": "*",
"react-native": "*"
}
},
"node_modules/expo-screen-orientation": {
"version": "57.0.1",
"resolved": "https://registry.npmjs.org/expo-screen-orientation/-/expo-screen-orientation-57.0.1.tgz",
+1
View File
@@ -13,6 +13,7 @@
"expo-file-system": "~56.0.8",
"expo-image-picker": "~56.0.18",
"expo-location": "~56.0.18",
"expo-navigation-bar": "^57.0.2",
"expo-screen-orientation": "^57.0.1",
"expo-secure-store": "~56.0.4",
"expo-sqlite": "~56.0.5",
+15
View File
@@ -1,6 +1,7 @@
import { CameraView, useCameraPermissions } from 'expo-camera';
import * as Location from 'expo-location';
import * as ScreenOrientation from 'expo-screen-orientation';
import * as NavigationBar from 'expo-navigation-bar';
import { useFocusEffect } from '@react-navigation/native';
import React, { useCallback, useRef, useState } from 'react';
import {
@@ -10,6 +11,8 @@ import {
View,
ActivityIndicator,
useWindowDimensions,
Platform,
StatusBar,
} from 'react-native';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { RootStackParamList } from '../navigation/types';
@@ -36,6 +39,13 @@ export function CameraScreen({ route, navigation }: Props) {
useCallback(() => {
let isMounted = true;
// Ocultar barras del sistema (Modo Inmersivo)
if (Platform.OS === 'android') {
void NavigationBar.setVisibilityAsync('hidden');
void NavigationBar.setBehaviorAsync('inset-touch');
}
StatusBar.setHidden(true);
const load = async () => {
const [cfg, projectId] = await Promise.all([
loadFooterConfig(),
@@ -95,6 +105,11 @@ export function CameraScreen({ route, navigation }: Props) {
return () => {
isMounted = false;
ScreenOrientation.removeOrientationChangeListener(sub);
// Restaurar barras del sistema al salir
if (Platform.OS === 'android') {
void NavigationBar.setVisibilityAsync('visible');
}
StatusBar.setHidden(false);
};
}, [])
);
+4 -10
View File
@@ -14,16 +14,9 @@ import { getTemplates } from '../db/repositories';
import { createInspection } from '../sync/mutations';
import { getDb } from '../db/database';
import { newUuid, nextTempId } from '../sync/uuid';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { RootStackParamList } from '../navigation/types';
import { MediaStrip } from '../ui/MediaStrip';
import {
Card,
ChipSelect,
COLORS,
Field,
PrimaryButton,
SectionTitle,
} from '../ui/components';
type Props = NativeStackScreenProps<RootStackParamList, 'InspectionForm'>;
@@ -78,6 +71,7 @@ function groupFields(fields: NormField[]): { group: string; items: NormField[] }
const RESULTS = ['pass', 'fail', 'na'] as const;
export function InspectionFormScreen({ route, navigation }: Props) {
const insets = useSafeAreaInsets();
const { featureId, featureName, templateId: suggestedId } = route.params;
// Paso 1: elegir plantilla (las asignadas al proyecto llegan en el bundle).
@@ -204,7 +198,7 @@ export function InspectionFormScreen({ route, navigation }: Props) {
// ── Paso 1: selector de plantilla ──
if (!chosen) {
return (
<ScrollView contentContainerStyle={styles.body}>
<ScrollView contentContainerStyle={[styles.body, { paddingBottom: insets.bottom + 20 }]}>
<Text style={styles.subtitle}>{featureName}</Text>
<Text style={styles.tplName}>Elige una plantilla</Text>
@@ -241,7 +235,7 @@ export function InspectionFormScreen({ route, navigation }: Props) {
// ── Paso 2: formulario ──
return (
<ScrollView contentContainerStyle={styles.body}>
<ScrollView contentContainerStyle={[styles.body, { paddingBottom: insets.bottom + 20 }]}>
<Text style={styles.subtitle}>{featureName}</Text>
<Text style={styles.tplName}>{template?.name ?? 'Inspección libre'}</Text>
<TouchableOpacity onPress={backToPicker}>
+3 -1
View File
@@ -7,6 +7,7 @@ import { getOutboxCounts, OutboxCounts } from '../db/outbox';
import { isOnline } from '../net/connectivity';
import { runSync } from '../sync/engine';
import { useAutoSync } from '../sync/useAutoSync';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { RootStackParamList } from '../navigation/types';
import { COLORS, PrimaryButton } from '../ui/components';
import { FeaturesSection } from './sections/FeaturesSection';
@@ -19,6 +20,7 @@ const TABS = ['Features', 'Incidencias'] as const;
type Tab = (typeof TABS)[number];
export function ProjectDetailScreen({ route, navigation }: Props) {
const insets = useSafeAreaInsets();
const { projectId } = route.params;
const [tab, setTab] = useState<Tab>('Features');
const [counts, setCounts] = useState<OutboxCounts>(EMPTY);
@@ -107,7 +109,7 @@ export function ProjectDetailScreen({ route, navigation }: Props) {
{tab === 'Incidencias' && <IssuesSection projectId={projectId} />}
</View>
<View style={styles.footer}>
<View style={[styles.footer, { paddingBottom: Math.max(12, insets.bottom) }]}>
<PrimaryButton
title={syncing ? 'Sincronizando…' : 'Sincronizar'}
onPress={() => void onSync()}
+4 -1
View File
@@ -18,6 +18,8 @@ import { runSync } from '../sync/engine';
import { RootStackParamList } from '../navigation/types';
import { COLORS } from '../ui/components';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
type Props = NativeStackScreenProps<RootStackParamList, 'Projects'>;
/** Tolerante con la forma de la respuesta: `{projects}` (actual), `{data}` o array. */
@@ -28,6 +30,7 @@ function normalize(res: unknown): Project[] {
}
export function ProjectsScreen({ navigation }: Props) {
const insets = useSafeAreaInsets();
const { user, signOut } = useSession();
const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(false);
@@ -105,7 +108,7 @@ export function ProjectsScreen({ navigation }: Props) {
};
return (
<View style={styles.container}>
<View style={[styles.container, { paddingBottom: insets.bottom }]}>
{/* Menu flotante (se posiciona relativo al contenedor principal) */}
{showMenu && (
<View style={styles.floatingMenu}>
+3 -1
View File
@@ -5,6 +5,7 @@ import { useFocusEffect, useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import React, { useCallback, useEffect, useState } from 'react';
import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { RootStackParamList } from '../../navigation/types';
import {
Badge,
@@ -17,6 +18,7 @@ import {
type Nav = NativeStackNavigationProp<RootStackParamList>;
export function FeatureDetailContent({ featureId }: { featureId: number }) {
const insets = useSafeAreaInsets();
const navigation = useNavigation<Nav>();
const { user } = useSession();
const canProgress = hasPermission(user, 'update progress');
@@ -81,7 +83,7 @@ export function FeatureDetailContent({ featureId }: { featureId: number }) {
const isActive = feature.is_active == null || Number(feature.is_active) !== 0;
return (
<ScrollView contentContainerStyle={styles.body}>
<ScrollView contentContainerStyle={[styles.body, { paddingBottom: insets.bottom + 20 }]}>
<View style={styles.badges}>
{featureType && (
<Badge label={featureType.name} color={featureType.color ?? COLORS.muted} />