POWR/docs/technical/styling/styling_guide.md
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

707 lines
21 KiB
Markdown

# 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
```typescript
// 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:
```jsx
// 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
```typescript
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
```jsx
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
```jsx
// 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
```jsx
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
```jsx
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
```jsx
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:
```jsx
<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
```jsx
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
```jsx
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**:
```jsx
// 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**:
```jsx
// 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**:
```jsx
// 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**:
```jsx
// 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**:
```jsx
// 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
```jsx
// 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
## Related Documentation
- [Coding Style Guide](../../guides/coding_style.md) - General coding patterns and practices
- [Component Architecture](../../architecture/index.md) - How components are organized
- [Accessibility Guidelines](../../guides/accessibility.md) - Making the app accessible to all users