831 lines
19 KiB
Markdown
831 lines
19 KiB
Markdown
|
|
# Sistema de Estilização e Componentes UI
|
||
|
|
|
||
|
|
## Visão Geral
|
||
|
|
|
||
|
|
O aplicativo utiliza um sistema de estilização híbrido, combinando StyleSheet nativo do React Native com Tailwind CSS para componentes web, além de uma biblioteca completa de componentes UI baseados no shadcn/ui.
|
||
|
|
|
||
|
|
## Sistema de Cores e Tema
|
||
|
|
|
||
|
|
### Paleta de Cores Principal
|
||
|
|
**Arquivo**: `src/constants/theme.ts`
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
export const COLORS = {
|
||
|
|
primary: "#0A1E63", // Azul escuro principal
|
||
|
|
secondary: "#F5F5F5", // Cinza claro
|
||
|
|
background: "#FFFFFF", // Branco de fundo
|
||
|
|
text: "#333333", // Cinza escuro para texto
|
||
|
|
textLight: "#777777", // Cinza médio para texto secundário
|
||
|
|
success: "#4CAF50", // Verde para sucesso
|
||
|
|
warning: "#FFC107", // Amarelo para avisos
|
||
|
|
danger: "#F44336", // Vermelho para erros
|
||
|
|
info: "#2196F3", // Azul para informações
|
||
|
|
border: "#E0E0E0", // Cinza claro para bordas
|
||
|
|
card: "#FFFFFF", // Branco para cards
|
||
|
|
shadow: "#000000", // Preto para sombras
|
||
|
|
error: "#FF3B30", // Vermelho para erros
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Sistema de Tamanhos
|
||
|
|
```typescript
|
||
|
|
export const SIZES = {
|
||
|
|
base: 8, // Unidade base (8px)
|
||
|
|
small: 12, // Pequeno
|
||
|
|
font: 14, // Tamanho de fonte padrão
|
||
|
|
medium: 16, // Médio
|
||
|
|
large: 18, // Grande
|
||
|
|
extraLarge: 24, // Extra grande
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Tipografia
|
||
|
|
```typescript
|
||
|
|
export const FONTS = {
|
||
|
|
regular: "Roboto-Regular",
|
||
|
|
medium: "Roboto-Medium",
|
||
|
|
bold: "Roboto-Bold",
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Sistema de Sombras
|
||
|
|
```typescript
|
||
|
|
export const SHADOWS = {
|
||
|
|
small: {
|
||
|
|
shadowColor: COLORS.shadow,
|
||
|
|
shadowOffset: { width: 0, height: 2 },
|
||
|
|
shadowOpacity: 0.25,
|
||
|
|
shadowRadius: 3.84,
|
||
|
|
elevation: 2,
|
||
|
|
},
|
||
|
|
medium: {
|
||
|
|
shadowColor: COLORS.shadow,
|
||
|
|
shadowOffset: { width: 0, height: 2 },
|
||
|
|
shadowOpacity: 0.25,
|
||
|
|
shadowRadius: 5.84,
|
||
|
|
elevation: 5,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Configuração Tailwind CSS
|
||
|
|
|
||
|
|
### Arquivo de Configuração
|
||
|
|
**Arquivo**: `tailwind.config.ts`
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
const config: Config = {
|
||
|
|
darkMode: ["class"],
|
||
|
|
content: [
|
||
|
|
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||
|
|
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||
|
|
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||
|
|
"*.{js,ts,jsx,tsx,mdx}"
|
||
|
|
],
|
||
|
|
theme: {
|
||
|
|
extend: {
|
||
|
|
colors: {
|
||
|
|
background: 'hsl(var(--background))',
|
||
|
|
foreground: 'hsl(var(--foreground))',
|
||
|
|
primary: {
|
||
|
|
DEFAULT: 'hsl(var(--primary))',
|
||
|
|
foreground: 'hsl(var(--primary-foreground))'
|
||
|
|
},
|
||
|
|
// ... outras cores
|
||
|
|
},
|
||
|
|
borderRadius: {
|
||
|
|
lg: 'var(--radius)',
|
||
|
|
md: 'calc(var(--radius) - 2px)',
|
||
|
|
sm: 'calc(var(--radius) - 4px)'
|
||
|
|
},
|
||
|
|
keyframes: {
|
||
|
|
'accordion-down': {
|
||
|
|
from: { height: '0' },
|
||
|
|
to: { height: 'var(--radix-accordion-content-height)' }
|
||
|
|
},
|
||
|
|
'accordion-up': {
|
||
|
|
from: { height: 'var(--radix-accordion-content-height)' },
|
||
|
|
to: { height: '0' }
|
||
|
|
}
|
||
|
|
},
|
||
|
|
animation: {
|
||
|
|
'accordion-down': 'accordion-down 0.2s ease-out',
|
||
|
|
'accordion-up': 'accordion-up 0.2s ease-out'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
plugins: [require("tailwindcss-animate")],
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
## Componentes UI Principais
|
||
|
|
|
||
|
|
### 1. Componente Icon
|
||
|
|
**Arquivo**: `components/Icon.tsx`
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
interface IconProps {
|
||
|
|
type: 'material' | 'font-awesome' | 'ionicons';
|
||
|
|
name: string;
|
||
|
|
size?: number;
|
||
|
|
color?: string;
|
||
|
|
style?: any;
|
||
|
|
}
|
||
|
|
|
||
|
|
const Icon: React.FC<IconProps> = ({ type, name, size = 24, color, style }) => {
|
||
|
|
switch (type) {
|
||
|
|
case 'material':
|
||
|
|
return <MaterialIcons name={name} size={size} color={color} style={style} />;
|
||
|
|
case 'font-awesome':
|
||
|
|
return <FontAwesome name={name} size={size} color={color} style={style} />;
|
||
|
|
case 'ionicons':
|
||
|
|
return <Ionicons name={name} size={size} color={color} style={style} />;
|
||
|
|
default:
|
||
|
|
return <MaterialIcons name={name} size={size} color={color} style={style} />;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
**Uso**:
|
||
|
|
```typescript
|
||
|
|
<Icon type="material" name="home" size={24} color={COLORS.primary} />
|
||
|
|
<Icon type="ionicons" name="location" size={20} color={COLORS.textLight} />
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. Componente FloatingPanicButton
|
||
|
|
**Arquivo**: `components/FloatingPanicButton.tsx`
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
interface FloatingPanicButtonProps {
|
||
|
|
onPanic: (location?: { latitude: number; longitude: number } | null) => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
const FloatingPanicButton: React.FC<FloatingPanicButtonProps> = ({ onPanic }) => {
|
||
|
|
const [location, setLocation] = useState<Location | null>(null);
|
||
|
|
|
||
|
|
const handlePanic = async () => {
|
||
|
|
try {
|
||
|
|
const { status } = await Location.requestForegroundPermissionsAsync();
|
||
|
|
if (status === 'granted') {
|
||
|
|
const location = await Location.getCurrentPositionAsync({});
|
||
|
|
setLocation(location.coords);
|
||
|
|
onPanic(location.coords);
|
||
|
|
} else {
|
||
|
|
onPanic(null);
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
onPanic(null);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<TouchableOpacity style={styles.panicButton} onPress={handlePanic}>
|
||
|
|
<Ionicons name="warning" size={24} color="white" />
|
||
|
|
</TouchableOpacity>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. Componente DeliveryMap
|
||
|
|
**Arquivo**: `src/components/DeliveryMap.tsx`
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
interface DeliveryMapProps {
|
||
|
|
deliveries: Delivery[];
|
||
|
|
onRouteCalculated?: (optimizedDeliveries: Delivery[]) => void;
|
||
|
|
showRoute?: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
const DeliveryMap: React.FC<DeliveryMapProps> = ({
|
||
|
|
deliveries,
|
||
|
|
onRouteCalculated,
|
||
|
|
showRoute = true
|
||
|
|
}) => {
|
||
|
|
const [mapRef, setMapRef] = useState<MapView | null>(null);
|
||
|
|
const [routeCoordinates, setRouteCoordinates] = useState<LatLng[]>([]);
|
||
|
|
|
||
|
|
// Implementação do mapa com marcadores e rotas
|
||
|
|
return (
|
||
|
|
<MapView
|
||
|
|
ref={setMapRef}
|
||
|
|
style={styles.map}
|
||
|
|
initialRegion={initialRegion}
|
||
|
|
onMapReady={handleMapReady}
|
||
|
|
>
|
||
|
|
{/* Marcadores das entregas */}
|
||
|
|
{deliveries.map((delivery, index) => (
|
||
|
|
<Marker
|
||
|
|
key={delivery.id}
|
||
|
|
coordinate={{
|
||
|
|
latitude: delivery.lat || 0,
|
||
|
|
longitude: delivery.lng || 0
|
||
|
|
}}
|
||
|
|
title={delivery.customerName}
|
||
|
|
description={`Entrega ${delivery.deliverySeq}`}
|
||
|
|
/>
|
||
|
|
))}
|
||
|
|
|
||
|
|
{/* Polilinha da rota */}
|
||
|
|
{showRoute && routeCoordinates.length > 0 && (
|
||
|
|
<Polyline
|
||
|
|
coordinates={routeCoordinates}
|
||
|
|
strokeColor={COLORS.primary}
|
||
|
|
strokeWidth={3}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
</MapView>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4. Componente MobileSignalIndicator
|
||
|
|
**Arquivo**: `src/components/MobileSignalIndicator.tsx`
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
interface MobileSignalIndicatorProps {
|
||
|
|
signalInfo: MobileSignalInfo;
|
||
|
|
onOfflineModeChange?: (isOffline: boolean) => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
const MobileSignalIndicator: React.FC<MobileSignalIndicatorProps> = ({
|
||
|
|
signalInfo,
|
||
|
|
onOfflineModeChange
|
||
|
|
}) => {
|
||
|
|
const getSignalColor = (strength: number) => {
|
||
|
|
if (strength >= 70) return COLORS.success;
|
||
|
|
if (strength >= 40) return COLORS.warning;
|
||
|
|
return COLORS.danger;
|
||
|
|
};
|
||
|
|
|
||
|
|
const getSignalIcon = (strength: number) => {
|
||
|
|
if (strength >= 70) return 'signal-cellular-4-bar';
|
||
|
|
if (strength >= 40) return 'signal-cellular-2-bar';
|
||
|
|
return 'signal-cellular-0-bar';
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<View style={styles.container}>
|
||
|
|
<Ionicons
|
||
|
|
name={getSignalIcon(signalInfo.signalStrength)}
|
||
|
|
size={16}
|
||
|
|
color={getSignalColor(signalInfo.signalStrength)}
|
||
|
|
/>
|
||
|
|
<Text style={styles.text}>
|
||
|
|
{signalInfo.signalStrength}% - {signalInfo.connectionType}
|
||
|
|
</Text>
|
||
|
|
{signalInfo.shouldUseOffline && (
|
||
|
|
<View style={styles.offlineBadge}>
|
||
|
|
<Text style={styles.offlineText}>OFFLINE</Text>
|
||
|
|
</View>
|
||
|
|
)}
|
||
|
|
</View>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
## Componentes shadcn/ui
|
||
|
|
|
||
|
|
### Estrutura dos Componentes
|
||
|
|
**Pasta**: `components/ui/`
|
||
|
|
|
||
|
|
Os componentes seguem o padrão shadcn/ui com adaptações para React Native:
|
||
|
|
|
||
|
|
#### 1. Button Component
|
||
|
|
**Arquivo**: `components/ui/button.tsx`
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
interface ButtonProps {
|
||
|
|
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
|
||
|
|
size?: 'default' | 'sm' | 'lg' | 'icon';
|
||
|
|
children: React.ReactNode;
|
||
|
|
onPress?: () => void;
|
||
|
|
disabled?: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
const Button: React.FC<ButtonProps> = ({
|
||
|
|
variant = 'default',
|
||
|
|
size = 'default',
|
||
|
|
children,
|
||
|
|
onPress,
|
||
|
|
disabled = false
|
||
|
|
}) => {
|
||
|
|
const buttonStyles = [
|
||
|
|
styles.base,
|
||
|
|
styles[variant],
|
||
|
|
styles[size],
|
||
|
|
disabled && styles.disabled
|
||
|
|
];
|
||
|
|
|
||
|
|
return (
|
||
|
|
<TouchableOpacity
|
||
|
|
style={buttonStyles}
|
||
|
|
onPress={onPress}
|
||
|
|
disabled={disabled}
|
||
|
|
>
|
||
|
|
{children}
|
||
|
|
</TouchableOpacity>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 2. Card Component
|
||
|
|
**Arquivo**: `components/ui/card.tsx`
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
interface CardProps {
|
||
|
|
children: React.ReactNode;
|
||
|
|
style?: any;
|
||
|
|
}
|
||
|
|
|
||
|
|
const Card: React.FC<CardProps> = ({ children, style }) => (
|
||
|
|
<View style={[styles.card, style]}>
|
||
|
|
{children}
|
||
|
|
</View>
|
||
|
|
);
|
||
|
|
|
||
|
|
const CardHeader: React.FC<CardProps> = ({ children, style }) => (
|
||
|
|
<View style={[styles.cardHeader, style]}>
|
||
|
|
{children}
|
||
|
|
</View>
|
||
|
|
);
|
||
|
|
|
||
|
|
const CardContent: React.FC<CardProps> = ({ children, style }) => (
|
||
|
|
<View style={[styles.cardContent, style]}>
|
||
|
|
{children}
|
||
|
|
</View>
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 3. Input Component
|
||
|
|
**Arquivo**: `components/ui/input.tsx`
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
interface InputProps {
|
||
|
|
placeholder?: string;
|
||
|
|
value?: string;
|
||
|
|
onChangeText?: (text: string) => void;
|
||
|
|
secureTextEntry?: boolean;
|
||
|
|
keyboardType?: KeyboardTypeOptions;
|
||
|
|
style?: any;
|
||
|
|
}
|
||
|
|
|
||
|
|
const Input: React.FC<InputProps> = ({
|
||
|
|
placeholder,
|
||
|
|
value,
|
||
|
|
onChangeText,
|
||
|
|
secureTextEntry = false,
|
||
|
|
keyboardType = 'default',
|
||
|
|
style
|
||
|
|
}) => (
|
||
|
|
<TextInput
|
||
|
|
style={[styles.input, style]}
|
||
|
|
placeholder={placeholder}
|
||
|
|
value={value}
|
||
|
|
onChangeText={onChangeText}
|
||
|
|
secureTextEntry={secureTextEntry}
|
||
|
|
keyboardType={keyboardType}
|
||
|
|
placeholderTextColor={COLORS.textLight}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
## Estilos das Telas Principais
|
||
|
|
|
||
|
|
### 1. LoginScreen Styles
|
||
|
|
```typescript
|
||
|
|
const styles = StyleSheet.create({
|
||
|
|
container: {
|
||
|
|
flex: 1,
|
||
|
|
backgroundColor: COLORS.background,
|
||
|
|
},
|
||
|
|
scrollContent: {
|
||
|
|
flexGrow: 1,
|
||
|
|
justifyContent: "center",
|
||
|
|
padding: 20,
|
||
|
|
},
|
||
|
|
logoContainer: {
|
||
|
|
alignItems: "center",
|
||
|
|
marginBottom: 20,
|
||
|
|
},
|
||
|
|
headerContainer: {
|
||
|
|
alignItems: "center",
|
||
|
|
marginBottom: 30,
|
||
|
|
},
|
||
|
|
headerTitle: {
|
||
|
|
fontSize: 28,
|
||
|
|
fontWeight: "bold",
|
||
|
|
color: COLORS.text,
|
||
|
|
marginBottom: 8,
|
||
|
|
},
|
||
|
|
formContainer: {
|
||
|
|
width: "100%",
|
||
|
|
maxWidth: 400,
|
||
|
|
alignSelf: "center",
|
||
|
|
},
|
||
|
|
input: {
|
||
|
|
backgroundColor: COLORS.secondary,
|
||
|
|
borderRadius: 12,
|
||
|
|
padding: 15,
|
||
|
|
fontSize: 16,
|
||
|
|
color: COLORS.text,
|
||
|
|
width: "100%",
|
||
|
|
},
|
||
|
|
loginButton: {
|
||
|
|
backgroundColor: COLORS.primary,
|
||
|
|
borderRadius: 12,
|
||
|
|
padding: 16,
|
||
|
|
alignItems: "center",
|
||
|
|
marginBottom: 24,
|
||
|
|
},
|
||
|
|
loginButtonText: {
|
||
|
|
color: "#FFFFFF",
|
||
|
|
fontSize: 16,
|
||
|
|
fontWeight: "bold",
|
||
|
|
},
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. HomeScreen Styles
|
||
|
|
```typescript
|
||
|
|
const styles = StyleSheet.create({
|
||
|
|
container: {
|
||
|
|
flex: 1,
|
||
|
|
backgroundColor: COLORS.background,
|
||
|
|
},
|
||
|
|
headerGradient: {
|
||
|
|
paddingBottom: 16,
|
||
|
|
},
|
||
|
|
header: {
|
||
|
|
flexDirection: "row",
|
||
|
|
alignItems: "flex-start",
|
||
|
|
justifyContent: "space-between",
|
||
|
|
paddingHorizontal: 20,
|
||
|
|
paddingVertical: 16,
|
||
|
|
},
|
||
|
|
statsContainer: {
|
||
|
|
flexDirection: "row",
|
||
|
|
backgroundColor: COLORS.card,
|
||
|
|
paddingVertical: 20,
|
||
|
|
paddingHorizontal: 20,
|
||
|
|
marginHorizontal: 16,
|
||
|
|
marginTop: -8,
|
||
|
|
borderRadius: 16,
|
||
|
|
...SHADOWS.medium,
|
||
|
|
},
|
||
|
|
statCard: {
|
||
|
|
flex: 1,
|
||
|
|
alignItems: "center",
|
||
|
|
},
|
||
|
|
statNumber: {
|
||
|
|
fontSize: 24,
|
||
|
|
fontWeight: "bold",
|
||
|
|
color: COLORS.primary,
|
||
|
|
marginBottom: 4,
|
||
|
|
},
|
||
|
|
nextDeliveryCard: {
|
||
|
|
backgroundColor: COLORS.card,
|
||
|
|
borderRadius: 20,
|
||
|
|
padding: 20,
|
||
|
|
marginHorizontal: 16,
|
||
|
|
marginBottom: 16,
|
||
|
|
...SHADOWS.medium,
|
||
|
|
borderWidth: 2,
|
||
|
|
borderColor: COLORS.primary + "20",
|
||
|
|
},
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. RoutesScreen Styles
|
||
|
|
```typescript
|
||
|
|
const styles = StyleSheet.create({
|
||
|
|
container: {
|
||
|
|
flex: 1,
|
||
|
|
backgroundColor: COLORS.background,
|
||
|
|
},
|
||
|
|
mapContainer: {
|
||
|
|
flex: 1,
|
||
|
|
borderRadius: 16,
|
||
|
|
overflow: 'hidden',
|
||
|
|
margin: 16,
|
||
|
|
...SHADOWS.medium,
|
||
|
|
},
|
||
|
|
map: {
|
||
|
|
flex: 1,
|
||
|
|
},
|
||
|
|
controlsContainer: {
|
||
|
|
position: 'absolute',
|
||
|
|
top: 20,
|
||
|
|
right: 20,
|
||
|
|
flexDirection: 'row',
|
||
|
|
gap: 10,
|
||
|
|
},
|
||
|
|
controlButton: {
|
||
|
|
backgroundColor: COLORS.card,
|
||
|
|
borderRadius: 8,
|
||
|
|
padding: 12,
|
||
|
|
...SHADOWS.small,
|
||
|
|
},
|
||
|
|
statsContainer: {
|
||
|
|
position: 'absolute',
|
||
|
|
bottom: 20,
|
||
|
|
left: 20,
|
||
|
|
right: 20,
|
||
|
|
backgroundColor: COLORS.card,
|
||
|
|
borderRadius: 16,
|
||
|
|
padding: 16,
|
||
|
|
...SHADOWS.medium,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
## Sistema de Gradientes
|
||
|
|
|
||
|
|
### LinearGradient Usage
|
||
|
|
```typescript
|
||
|
|
import { LinearGradient } from 'expo-linear-gradient';
|
||
|
|
|
||
|
|
// Header gradient
|
||
|
|
<LinearGradient
|
||
|
|
colors={[COLORS.primary, "#3B82F6"]}
|
||
|
|
style={styles.headerGradient}
|
||
|
|
>
|
||
|
|
{/* Header content */}
|
||
|
|
</LinearGradient>
|
||
|
|
|
||
|
|
// Button gradient
|
||
|
|
<LinearGradient
|
||
|
|
colors={[COLORS.primary, "#3B82F6"]}
|
||
|
|
style={styles.buttonGradient}
|
||
|
|
start={{ x: 0, y: 0 }}
|
||
|
|
end={{ x: 1, y: 1 }}
|
||
|
|
>
|
||
|
|
<Text style={styles.buttonText}>Entrar</Text>
|
||
|
|
</LinearGradient>
|
||
|
|
|
||
|
|
// Card gradient
|
||
|
|
<LinearGradient
|
||
|
|
colors={["#D1FAE5", "#F0FDF4"]}
|
||
|
|
style={styles.successCard}
|
||
|
|
start={{ x: 0, y: 0 }}
|
||
|
|
end={{ x: 0, y: 1 }}
|
||
|
|
>
|
||
|
|
{/* Success content */}
|
||
|
|
</LinearGradient>
|
||
|
|
```
|
||
|
|
|
||
|
|
## Animações e Transições
|
||
|
|
|
||
|
|
### Animated Components
|
||
|
|
```typescript
|
||
|
|
import { Animated } from 'react-native';
|
||
|
|
|
||
|
|
// Fade animation
|
||
|
|
const fadeAnim = useRef(new Animated.Value(0)).current;
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
Animated.timing(fadeAnim, {
|
||
|
|
toValue: 1,
|
||
|
|
duration: 300,
|
||
|
|
useNativeDriver: true,
|
||
|
|
}).start();
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
<Animated.View style={{ opacity: fadeAnim }}>
|
||
|
|
{/* Animated content */}
|
||
|
|
</Animated.View>
|
||
|
|
|
||
|
|
// Scale animation
|
||
|
|
const scaleAnim = useRef(new Animated.Value(0)).current;
|
||
|
|
|
||
|
|
const animatePress = () => {
|
||
|
|
Animated.sequence([
|
||
|
|
Animated.timing(scaleAnim, {
|
||
|
|
toValue: 0.95,
|
||
|
|
duration: 100,
|
||
|
|
useNativeDriver: true,
|
||
|
|
}),
|
||
|
|
Animated.timing(scaleAnim, {
|
||
|
|
toValue: 1,
|
||
|
|
duration: 100,
|
||
|
|
useNativeDriver: true,
|
||
|
|
}),
|
||
|
|
]).start();
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
## Responsividade e Adaptação
|
||
|
|
|
||
|
|
### Dimensions Usage
|
||
|
|
```typescript
|
||
|
|
import { Dimensions } from 'react-native';
|
||
|
|
|
||
|
|
const { width, height } = Dimensions.get('window');
|
||
|
|
|
||
|
|
// Responsive styles
|
||
|
|
const styles = StyleSheet.create({
|
||
|
|
container: {
|
||
|
|
width: width * 0.9,
|
||
|
|
height: height * 0.8,
|
||
|
|
},
|
||
|
|
card: {
|
||
|
|
width: width > 768 ? width * 0.3 : width * 0.9,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### Platform-specific Styles
|
||
|
|
```typescript
|
||
|
|
import { Platform } from 'react-native';
|
||
|
|
|
||
|
|
const styles = StyleSheet.create({
|
||
|
|
header: {
|
||
|
|
paddingTop: Platform.OS === 'ios' ? 44 : 24,
|
||
|
|
},
|
||
|
|
button: {
|
||
|
|
borderRadius: Platform.OS === 'ios' ? 8 : 4,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
## Temas e Modo Escuro
|
||
|
|
|
||
|
|
### Theme Provider
|
||
|
|
```typescript
|
||
|
|
// components/theme-provider.tsx
|
||
|
|
interface ThemeContextType {
|
||
|
|
theme: 'light' | 'dark';
|
||
|
|
toggleTheme: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
const ThemeContext = createContext<ThemeContextType>({
|
||
|
|
theme: 'light',
|
||
|
|
toggleTheme: () => {},
|
||
|
|
});
|
||
|
|
|
||
|
|
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||
|
|
const [theme, setTheme] = useState<'light' | 'dark'>('light');
|
||
|
|
|
||
|
|
const toggleTheme = () => {
|
||
|
|
setTheme(prev => prev === 'light' ? 'dark' : 'light');
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
||
|
|
{children}
|
||
|
|
</ThemeContext.Provider>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### Dark Mode Colors
|
||
|
|
```typescript
|
||
|
|
export const DARK_COLORS = {
|
||
|
|
primary: "#1E40AF",
|
||
|
|
secondary: "#1F2937",
|
||
|
|
background: "#111827",
|
||
|
|
text: "#F9FAFB",
|
||
|
|
textLight: "#9CA3AF",
|
||
|
|
card: "#1F2937",
|
||
|
|
border: "#374151",
|
||
|
|
// ... outras cores
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
## Componentes de Formulário
|
||
|
|
|
||
|
|
### Form Components
|
||
|
|
```typescript
|
||
|
|
// components/ui/form.tsx
|
||
|
|
interface FormFieldProps {
|
||
|
|
label: string;
|
||
|
|
error?: string;
|
||
|
|
children: React.ReactNode;
|
||
|
|
}
|
||
|
|
|
||
|
|
const FormField: React.FC<FormFieldProps> = ({ label, error, children }) => (
|
||
|
|
<View style={styles.formField}>
|
||
|
|
<Text style={styles.label}>{label}</Text>
|
||
|
|
{children}
|
||
|
|
{error && <Text style={styles.errorText}>{error}</Text>}
|
||
|
|
</View>
|
||
|
|
);
|
||
|
|
|
||
|
|
// Usage
|
||
|
|
<FormField label="Nome de Usuário" error={usernameError}>
|
||
|
|
<Input
|
||
|
|
placeholder="Digite seu nome de usuário"
|
||
|
|
value={username}
|
||
|
|
onChangeText={setUsername}
|
||
|
|
/>
|
||
|
|
</FormField>
|
||
|
|
```
|
||
|
|
|
||
|
|
## Componentes de Feedback
|
||
|
|
|
||
|
|
### Toast Component
|
||
|
|
```typescript
|
||
|
|
// components/ui/toast.tsx
|
||
|
|
interface ToastProps {
|
||
|
|
message: string;
|
||
|
|
type: 'success' | 'error' | 'warning' | 'info';
|
||
|
|
visible: boolean;
|
||
|
|
onHide: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
const Toast: React.FC<ToastProps> = ({ message, type, visible, onHide }) => {
|
||
|
|
useEffect(() => {
|
||
|
|
if (visible) {
|
||
|
|
const timer = setTimeout(onHide, 3000);
|
||
|
|
return () => clearTimeout(timer);
|
||
|
|
}
|
||
|
|
}, [visible, onHide]);
|
||
|
|
|
||
|
|
if (!visible) return null;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Animated.View style={[styles.toast, styles[type]]}>
|
||
|
|
<Ionicons name={getIcon(type)} size={20} color="white" />
|
||
|
|
<Text style={styles.toastText}>{message}</Text>
|
||
|
|
</Animated.View>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### Loading Component
|
||
|
|
```typescript
|
||
|
|
// components/ui/loading.tsx
|
||
|
|
interface LoadingProps {
|
||
|
|
visible: boolean;
|
||
|
|
message?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
const Loading: React.FC<LoadingProps> = ({ visible, message = "Carregando..." }) => {
|
||
|
|
if (!visible) return null;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<View style={styles.overlay}>
|
||
|
|
<View style={styles.loadingContainer}>
|
||
|
|
<ActivityIndicator size="large" color={COLORS.primary} />
|
||
|
|
<Text style={styles.loadingText}>{message}</Text>
|
||
|
|
</View>
|
||
|
|
</View>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
## Considerações para Sincronização Offline
|
||
|
|
|
||
|
|
### 1. Indicadores de Status
|
||
|
|
```typescript
|
||
|
|
// Componente para mostrar status de sincronização
|
||
|
|
const SyncStatusIndicator: React.FC<{ status: 'synced' | 'pending' | 'error' }> = ({ status }) => {
|
||
|
|
const getStatusColor = (status: string) => {
|
||
|
|
switch (status) {
|
||
|
|
case 'synced': return COLORS.success;
|
||
|
|
case 'pending': return COLORS.warning;
|
||
|
|
case 'error': return COLORS.danger;
|
||
|
|
default: return COLORS.textLight;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const getStatusIcon = (status: string) => {
|
||
|
|
switch (status) {
|
||
|
|
case 'synced': return 'checkmark-circle';
|
||
|
|
case 'pending': return 'time';
|
||
|
|
case 'error': return 'close-circle';
|
||
|
|
default: return 'help-circle';
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<View style={styles.statusContainer}>
|
||
|
|
<Ionicons
|
||
|
|
name={getStatusIcon(status)}
|
||
|
|
size={16}
|
||
|
|
color={getStatusColor(status)}
|
||
|
|
/>
|
||
|
|
<Text style={[styles.statusText, { color: getStatusColor(status) }]}>
|
||
|
|
{status.toUpperCase()}
|
||
|
|
</Text>
|
||
|
|
</View>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. Componentes Offline
|
||
|
|
```typescript
|
||
|
|
// Componente para modo offline
|
||
|
|
const OfflineBanner: React.FC<{ visible: boolean }> = ({ visible }) => {
|
||
|
|
if (!visible) return null;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<View style={styles.offlineBanner}>
|
||
|
|
<Ionicons name="cloud-offline" size={20} color="white" />
|
||
|
|
<Text style={styles.offlineText}>Modo Offline Ativo</Text>
|
||
|
|
</View>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
Esta documentação fornece uma visão completa do sistema de estilização e componentes UI do aplicativo, incluindo padrões de design, componentes reutilizáveis e considerações para implementação de funcionalidades offline.
|