feat: scaffold offline-first mobile app (RN+Expo, expo-sqlite)

Capa API tipada de los 8 endpoints, BD local espejo del bundle + outbox
(operaciones y media) + cursor de sync, motor runSync (PUSH /sync ->
PUSH /media -> PULL bundle?since) con idempotencia por uuid y last-write-wins,
mutaciones de alto nivel (write local + encolar), sesion con token en
SecureStore, conectividad NetInfo y UI minima (Login -> Proyectos -> Detalle).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
javier
2026-06-18 17:43:48 +02:00
co-authored by Claude Opus 4.8
parent 3f454b59a5
commit 4e9c7d059f
27 changed files with 9057 additions and 20 deletions
+38
View File
@@ -0,0 +1,38 @@
import React from 'react';
import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
import { OutboxCounts } from '../db/outbox';
import { useIsOnline } from '../net/connectivity';
export function SyncStatusBar({
counts,
syncing,
}: {
counts: OutboxCounts;
syncing: boolean;
}) {
const online = useIsOnline();
const pendientes = counts.pending + counts.mediaPending;
return (
<View style={[styles.bar, { backgroundColor: online ? '#1f6f43' : '#8a6d00' }]}>
{syncing && <ActivityIndicator color="#fff" size="small" />}
<Text style={styles.text}>
{online ? 'En línea' : 'Sin conexión'}
{pendientes > 0 ? ` · ${pendientes} en cola` : ' · al día'}
{counts.conflict > 0 ? ` · ${counts.conflict} conflicto(s)` : ''}
{counts.error > 0 ? ` · ${counts.error} error(es)` : ''}
</Text>
</View>
);
}
const styles = StyleSheet.create({
bar: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
paddingHorizontal: 12,
paddingVertical: 8,
},
text: { color: '#fff', fontSize: 13, fontWeight: '600' },
});