entregas_app/docs/SISTEMA_ESTILIZACAO_UI.md

19 KiB

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

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

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

export const FONTS = {
  regular: "Roboto-Regular",
  medium: "Roboto-Medium", 
  bold: "Roboto-Bold",
}

Sistema de Sombras

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

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

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:

<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

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

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

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

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

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

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

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

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

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

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

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

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

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

// 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

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

// 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

// 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

// 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

// 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

// 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.