1
0
mirror of https://github.com/DocNR/POWR.git synced 2025-05-19 16:32:07 +00:00
DocNR 969163313a fix(auth): Improve authentication state handling and avatar display
* Add style prop to UserAvatar component for better customization
* Refactor UserAvatar to use getAvatarSeed utility for consistent avatar generation
* Fix React hook ordering issues in profile/overview.tsx to prevent crashes during auth state changes
* Add proper state initialization and cleanup during authentication transitions
* Ensure consistent fallback avatar display for unauthenticated users

These changes improve stability during login/logout operations and provide better visual continuity with Robohash avatars when profile images aren't available.
2025-04-02 21:11:25 -04:00

21 KiB

POWR App Styling Guide

Last Updated: 2025-04-01
Status: Active
Related To: Design System, Component Architecture, UI/UX, Cross-Platform Development

Purpose

This document outlines the styling principles, component usage patterns, and theming implementation for the POWR fitness app. Following these guidelines ensures a consistent look and feel across the application, facilitates cross-platform compatibility, and enhances the overall user experience.

Cross-Platform Approach

The POWR app is designed to run on both iOS and Android platforms, which present unique challenges for UI implementation. Our approach prioritizes:

  1. Platform Consistency: Maintaining a consistent look and feel across platforms
  2. Platform Adaptation: Respecting platform-specific UX patterns where appropriate
  3. Graceful Fallbacks: Implementing fallbacks for features not available on all platforms
  4. Testing on Both Platforms: All UI changes must be verified on both iOS and Android

Theme System Architecture

The POWR app uses a flexible theme system built with React Native, Tailwind CSS, and shadcn/ui components. The theming infrastructure supports both light and dark modes, with dynamic color adjustments for different UI states.

Theme File Organization

lib/theme/
  ├── index.ts       - Main theme export
  ├── colors.ts      - Color definitions
  ├── constants.ts   - Theme constants 
  ├── iconUtils.ts   - Icon styling utilities
  └── useColorScheme.tsx - Theme mode selection hook

Theme Implementation Strategy

The application uses:

  • Tailwind classes for general styling with nativewind
  • Specialized hooks for cross-platform compatibility (useIconColor, etc.)
  • shadcn/ui component library for consistent UI elements

Color System

All colors should be accessed through the theme system rather than using hardcoded values. Never use direct color codes in components.

Color Imports

// Import theme utilities
import { useTheme } from '@/lib/theme';
import { useIconColor } from '@/lib/theme/iconUtils';
import { FIXED_COLORS } from '@/lib/theme/colors';

Color Variants

The theme includes semantic color variants for different UI elements:

  • primary - Brand color, used for main interactive elements (purple)
  • secondary - Supporting UI elements
  • muted - Subdued elements, backgrounds, disabled states
  • accent - Highlights and accents
  • destructive - Error states, deletion actions (red)
  • success - Confirmation, completion states (green)
  • warning - Caution states (yellow/orange)

Accessing Colors

Always access colors through Tailwind classes:

// Good - uses theme system
<View className="bg-primary rounded-md p-4">
  <Text className="text-primary-foreground font-medium">
    Hello World
  </Text>
</View>

// Bad - hardcoded values that won't respond to theme changes
<View style={{ backgroundColor: '#8B5CF6', borderRadius: 8, padding: 16 }}>
  <Text style={{ color: '#FFFFFF', fontWeight: 500 }}>
    Hello World
  </Text>
</View>

Icon Styling

Icons must use the icon utility functions to ensure visibility across platforms. Different platforms may require different stroke widths, colors, and other properties.

Icon Usage

import { useIconColor } from '@/lib/theme/iconUtils';
import { Play, Star, Trash2 } from 'lucide-react-native';

// Inside your functional component
function MyComponent() {
  const { getIconProps, getIconColor } = useIconColor();
  
  return (
    <View>
      {/* Primary action icon */}
      <Play {...getIconProps('primary')} size={20} />

      {/* Destructive action icon */}
      <Trash2 {...getIconProps('destructive')} size={20} />

      {/* Icon with conditional fill */}
      <Star 
        {...getIconProps(isFavorite ? 'primary' : 'muted')}
        fill={isFavorite ? getIconColor('primary') : "none"}
        size={20} 
      />
    </View>
  );
}

Icon Variants

  • primary - For main actions and interactive elements
  • muted - For secondary or less important actions
  • destructive - For delete/remove actions
  • success - For confirmation/complete actions
  • warning - For caution indicators

Platform-Specific Icon Considerations

  • Android:

    • Icons often appear thinner and less visible on Android
    • Always use strokeWidth={2} or higher for better visibility on Android
    • Minimum recommended icon size is 24px for Android (vs. 20px for iOS)
    • Use the getIconProps function which handles these platform differences automatically
  • iOS:

    • Icons generally appear as expected with default stroke width
    • iOS has better support for gradients and complex icon styles

Button Styling

Use the standard Button component with appropriate variants to maintain a consistent look and feel.

Button Variants

import { Button } from '@/components/ui/button';
import { Text } from '@/components/ui/text';

// Primary button
<Button variant="default" className="w-full">
  <Text className="text-primary-foreground">Primary Action</Text>
</Button>

// Destructive button
<Button variant="destructive" className="w-full">
  <Text className="text-destructive-foreground">Delete</Text>
</Button>

// Outline button
<Button variant="outline" className="w-full">
  <Text>Secondary Action</Text>
</Button>

// Ghost button (minimal visual impact)
<Button variant="ghost" className="w-full">
  <Text>Subtle Action</Text>
</Button>

// Link button
<Button variant="link" className="w-full">
  <Text className="text-primary underline">Learn More</Text>
</Button>

Button States

Buttons handle the following states automatically through the theme system:

  • Default
  • Hover/active (handled differently on mobile and web)
  • Disabled
  • Loading
// Disabled button
<Button variant="default" disabled className="w-full">
  <Text className="text-primary-foreground">Unavailable</Text>
</Button>

// Loading button
<Button variant="default" isLoading className="w-full">
  <Text className="text-primary-foreground">Loading...</Text>
</Button>

Platform-Specific Button Considerations

  • Android:

    • Android buttons may need additional padding to match iOS visual weight
    • Use android:elevation or equivalent shadow values for proper elevation on Android
    • Ripple effects require additional configuration to work properly
    • Consider using TouchableNativeFeedback for Android-specific feedback on buttons
  • iOS:

    • iOS buttons typically have more subtle feedback effects
    • Shadow properties work more predictably on iOS

Header Component

Use the Header component consistently across all screens for navigation and context.

Header Configuration

import { Header } from '@/components/Header';

// Standard header with title
<Header title="Screen Title" showNotifications={true} />

// Header with logo
<Header useLogo={true} showNotifications={true} />

// Header with custom right element
<Header 
  title="Screen Title" 
  rightElement={<YourCustomElement />} 
/>

// Header with back button
<Header 
  title="Details" 
  showBackButton={true} 
  onBack={() => navigation.goBack()} 
/>

Platform-Specific Header Considerations

  • Android:

    • Android status bar customization requires StatusBar component with platform checks
    • Text in headers may render differently, requiring platform-specific adjustments
    • Back button styling differs between platforms - use the Header component's built-in options
    • Shadow effects need to be handled differently on Android (elevation vs shadowProps)
  • iOS:

    • iOS has native support for large titles and collapsible headers
    • Safe area insets are critical for proper header positioning on iOS
    • Status bar content color changes (dark/light) may need to be explicitly specified

Text Styling

Use the Text component with appropriate Tailwind classes for typography. This ensures the correct font styles across platforms.

Text Hierarchy

import { Text } from '@/components/ui/text';

// Page title
<Text className="text-2xl font-bold text-foreground">
  Page Title
</Text>

// Section heading
<Text className="text-xl font-semibold text-foreground mb-2">
  Section Heading
</Text>

// Subsection heading
<Text className="text-lg font-medium text-foreground mb-1">
  Subsection Heading
</Text>

// Body text
<Text className="text-base text-foreground">
  Regular body text for primary content.
</Text>

// Secondary text
<Text className="text-sm text-muted-foreground">
  Secondary information or supporting text.
</Text>

// Small text / captions
<Text className="text-xs text-muted-foreground">
  Caption text, timestamps, etc.
</Text>

Platform-Specific Text Considerations

  • Android:

    • Font rendering is different on Android - text may appear smaller or thinner
    • Android requires explicit fontFamily specification for custom fonts
    • Line height calculations differ between platforms - may need adjustments
    • Some text styling properties like letterSpacing work differently on Android
    • Use includeFontPadding: false on Android to fix inconsistent text height
  • iOS:

    • Dynamic Type (iOS accessibility feature) should be supported
    • Certain text styles like small caps require different implementations
    • Font weights map differently between platforms (400 on iOS may not look the same as 400 on Android)

Card Components

Use the Card component family for content blocks throughout the app.

Basic Card

import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';

<Card className="mx-4 mb-4">
  <CardHeader>
    <CardTitle>
      <Text className="text-lg font-semibold">Card Title</Text>
    </CardTitle>
  </CardHeader>
  <CardContent className="p-4">
    <Text className="text-foreground">
      Card content goes here.
    </Text>
  </CardContent>
  <CardFooter className="flex-row justify-between px-4 py-2">
    <Button variant="ghost" size="sm">
      <Text>Cancel</Text>
    </Button>
    <Button variant="default" size="sm">
      <Text className="text-primary-foreground">Confirm</Text>
    </Button>
  </CardFooter>
</Card>

Interactive Card

For cards that function as buttons:

<Pressable onPress={handlePress}>
  <Card className="mx-4 mb-4 border-l-4 border-l-primary">
    <CardContent className="p-4">
      <Text className="text-foreground font-medium">
        Interactive Card
      </Text>
      <Text className="text-sm text-muted-foreground mt-1">
        Tap to interact
      </Text>
    </CardContent>
  </Card>
</Pressable>

Platform-Specific Card Considerations

  • Android:

    • Use elevation for shadows on Android instead of shadow-* classes
    • Border radius may render differently on older Android versions
    • Ripple effects for interactive cards need platform-specific configuration
    • Border styles may appear differently on Android
  • iOS:

    • Shadow properties work more predictably on iOS
    • Cards with dynamic height may need additional configuration for iOS

Dialog/Alert Styling

Center buttons in dialogs for better usability and maintain consistent styling for these components.

Alert Dialog Example

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
} from '@/components/ui/alert-dialog';

<AlertDialog>
  <AlertDialogContent>
    <AlertDialogHeader>
      <AlertDialogTitle>
        <Text className="text-lg font-semibold text-foreground">
          Confirm Action
        </Text>
      </AlertDialogTitle>
      <AlertDialogDescription>
        <Text className="text-muted-foreground">
          Are you sure you want to continue? This action cannot be undone.
        </Text>
      </AlertDialogDescription>
    </AlertDialogHeader>
    <AlertDialogFooter className="flex-row justify-center gap-3 mt-4">
      <AlertDialogCancel>
        <Text>Cancel</Text>
      </AlertDialogCancel>
      <AlertDialogAction className="bg-destructive">
        <Text className="text-destructive-foreground">Confirm</Text>
      </AlertDialogAction>
    </AlertDialogFooter>
  </AlertDialogContent>
</AlertDialog>

Platform-Specific Dialog Considerations

  • Android:

    • Android dialogs traditionally have buttons aligned to the right
    • Back button behavior needs special handling on Android dialogs
    • Touch outside to dismiss works differently on Android
    • Dialog animations differ between platforms
    • Material Design guidelines suggest different spacing and typography than iOS
  • iOS:

    • iOS dialogs typically have vertically stacked buttons
    • Safe area insets must be respected on full-screen iOS sheets
    • iOS has specific swipe gestures for sheet dismissal

Form Elements

Style form elements consistently for a coherent user experience.

Form Field Examples

import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';

// Text input with label
<View className="mb-4">
  <Label htmlFor="name" className="mb-1.5">
    <Text className="text-sm font-medium">Name</Text>
  </Label>
  <Input
    id="name"
    placeholder="Enter your name"
    value={name}
    onChangeText={setName}
    className="bg-background"
  />
  {error && (
    <Text className="text-xs text-destructive mt-1">
      {error}
    </Text>
  )}
</View>

// Select input with label
<View className="mb-4">
  <Label htmlFor="category" className="mb-1.5">
    <Text className="text-sm font-medium">Category</Text>
  </Label>
  <Select
    id="category"
    value={category}
    onValueChange={setCategory}
    className="bg-background"
  >
    {categories.map(cat => (
      <SelectItem key={cat.id} label={cat.name} value={cat.id} />
    ))}
  </Select>
</View>

Platform-Specific Form Element Considerations

  • Android:

    • Input fields may need additional padding or height adjustments
    • Text field focus appearance differs significantly (Material Design vs. iOS)
    • Android requires explicit configuration for soft keyboard behavior
    • Date/time pickers have completely different UIs between platforms
    • Dropdown selects appear and behave differently on Android
  • iOS:

    • Form elements typically have a lighter visual style
    • iOS has specific picker components that are different from Android
    • Keyboard accessories are common on iOS but less so on Android
    • Text selection handles and behavior differ between platforms

Common Cross-Platform Issues and Solutions

Shadow Implementation

Issue: Shadow styling works differently between iOS and Android.

Solution:

// Cross-platform shadow solution
<View
  className="bg-card rounded-lg p-4"
  style={Platform.select({
    ios: {
      shadowColor: '#000',
      shadowOffset: { width: 0, height: 2 },
      shadowOpacity: 0.1,
      shadowRadius: 4,
    },
    android: {
      elevation: 4,
    },
  })}
>
  <Text>Content with consistent shadow across platforms</Text>
</View>

Icon Rendering

Issue: Icons appear properly on iOS but are barely visible on Android.

Solution:

// Always use the icon utility
import { useIconColor } from '@/lib/theme/iconUtils';

function MyComponent() {
  const { getIconProps } = useIconColor();
  
  return (
    <Icon
      {...getIconProps('primary')}
      size={24} // Slightly larger for Android
      strokeWidth={Platform.OS === 'android' ? 2 : 1.5} // Explicit adjustment
    />
  );
}

Text Alignment

Issue: Text alignment and truncation behaves differently across platforms.

Solution:

// Text alignment helper component
function AlignedText({ children, ...props }) {
  return (
    <Text
      {...props}
      style={[
        props.style,
        Platform.OS === 'android' ? { includeFontPadding: false } : null,
        Platform.OS === 'android' ? { lineHeight: 24 } : null,
      ]}
    >
      {children}
    </Text>
  );
}

Touchable Feedback

Issue: Touch feedback effects differ between platforms.

Solution:

// Platform-specific touchable
function AppTouchable({ children, onPress, ...props }) {
  if (Platform.OS === 'android') {
    return (
      <TouchableNativeFeedback
        onPress={onPress}
        background={TouchableNativeFeedback.Ripple('#rgba(0,0,0,0.1)', false)}
        {...props}
      >
        <View>{children}</View>
      </TouchableNativeFeedback>
    );
  }
  
  return (
    <TouchableOpacity onPress={onPress} activeOpacity={0.7} {...props}>
      {children}
    </TouchableOpacity>
  );
}

Keyboard Handling

Issue: Keyboard behavior and avoidance differs between platforms.

Solution:

// Keyboard handling helper
<KeyboardAvoidingView
  behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
  keyboardVerticalOffset={Platform.OS === 'ios' ? 64 : 0}
  style={{ flex: 1 }}
>
  <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
    <View style={{ flex: 1 }}>
      {/* Form content */}
    </View>
  </TouchableWithoutFeedback>
</KeyboardAvoidingView>

Platform-Specific Component Extensions

For cases where significant platform differences exist, create platform-specific component extensions:

Example: Platform-Specific DatePicker

// DatePickerWrapper.jsx
import { DatePicker } from './DatePicker.ios';
import { DatePicker } from './DatePicker.android';

export const DatePickerWrapper = (props) => {
  const Component = Platform.select({
    ios: DatePickerIOS,
    android: DatePickerAndroid,
  });
  
  return <Component {...props} />;
};

Best Practices for Cross-Platform Development

  1. Always test on both platforms before considering a feature complete
  2. Use platform detection judiciously - prefer cross-platform solutions where possible
  3. Create abstraction layers for significantly different platform components
  4. Leverage UI component libraries that handle cross-platform differences (like UI Kitten, React Native Paper)
  5. Document platform-specific quirks that you encounter for future reference
  6. Create utility functions for common platform-specific adjustments
  7. Use feature detection instead of platform detection when possible
  8. Consider native device capabilities like haptic feedback that may not exist on all devices

Best Practices for POWR App Styling

  1. Never use hardcoded colors - Always use theme variables through Tailwind classes
  2. Always use getIconProps for icons - Ensures visibility on both iOS and Android
  3. Use semantic variants - Choose button and icon variants based on their purpose
  4. Maintain consistent spacing - Use Tailwind spacing classes (p-4, m-2, etc.)
  5. Test both platforms - Verify UI rendering on both iOS and Android
  6. Use platform-specific overrides when necessary
  7. Document platform-specific behavior in component comments

Troubleshooting Common Issues

Icons Not Visible on Android

Problem: Icons don't appear or are difficult to see on Android devices.

Solution:

  • Ensure you're using getIconProps() instead of direct styling
  • Add strokeWidth={2} to increase visibility
  • Verify that icon size is appropriate (min 24px recommended for Android)
  • Check that the icon color has sufficient contrast with the background

Inconsistent Colors

Problem: Colors appear inconsistent between components or platforms.

Solution:

  • Verify you're using Tailwind classes (text-primary vs #8B5CF6)
  • Check that the correct variant is being used for the component
  • Ensure components are properly wrapped with theme provider
  • Examine component hierarchy for style inheritance issues

Text Truncation Issues

Problem: Text doesn't truncate properly or layout breaks with long content.

Solution:

  • Add numberOfLines={1} for single-line truncation
  • Use ellipsizeMode="tail" for text truncation
  • Wrap Text components with a fixed-width container
  • Consider using a more robust solution for responsive text
  • Apply platform-specific text style adjustments

Shadow and Elevation

Problem: Shadows appear on iOS but not on Android, or look inconsistent.

Solution:

  • Use platform-specific shadow implementation (see example above)
  • For Android, use elevation property
  • For iOS, use shadowColor, shadowOffset, shadowOpacity, and shadowRadius
  • Test shadow values on different Android versions

Keyboard Issues

Problem: Keyboard covers input fields or doesn't dismiss properly.

Solution:

  • Use KeyboardAvoidingView with platform-specific behavior
  • Implement Keyboard.dismiss on background taps
  • Add ScrollView for forms to ensure all fields are accessible
  • Consider using a keyboard manager library for complex forms