diff --git a/App.tsx b/App.tsx
index efbcb62..a1d1e04 100644
--- a/App.tsx
+++ b/App.tsx
@@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react';
import { NavigationContainer, LinkingOptions } from '@react-navigation/native';
import { AuthProvider } from './src/contexts/AuthContext';
+import { AccessibilityProvider } from './src/contexts/AccessibilityContext';
import { useAuth } from './src/hooks/useAuth';
import SplashScreen from './src/screens/SplashScreen';
import AuthNavigator from './src/navigation/AuthNavigator';
@@ -61,18 +62,20 @@ function RootNavigator() {
export default function App() {
return (
-
-
- linking={linkingConfig}
- fallback={
-
-
-
- }
- >
-
-
-
+
+
+
+ linking={linkingConfig}
+ fallback={
+
+
+
+ }
+ >
+
+
+
+
);
}
diff --git a/jest.config.js b/jest.config.js
index 6716ca1..1cdb148 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -8,5 +8,6 @@ module.exports = {
'node_modules/(?!(react-native|@react-native|@react-navigation)/)'
],
testPathIgnorePatterns: ['/node_modules/', '/android/', '/ios/'],
+ setupFiles: ['/jest.setup.js'],
resetMocks: true,
};
\ No newline at end of file
diff --git a/jest.setup.js b/jest.setup.js
new file mode 100644
index 0000000..5ede553
--- /dev/null
+++ b/jest.setup.js
@@ -0,0 +1,3 @@
+jest.mock('@react-native-async-storage/async-storage', () =>
+ require('@react-native-async-storage/async-storage/jest/async-storage-mock'),
+);
diff --git a/src/components/HorizontalMenu.tsx b/src/components/HorizontalMenu.tsx
index 79248a7..658930a 100644
--- a/src/components/HorizontalMenu.tsx
+++ b/src/components/HorizontalMenu.tsx
@@ -1,6 +1,8 @@
-import React from 'react';
+import React, { useMemo } from 'react';
import { ScrollView, TouchableOpacity, Text, StyleSheet } from 'react-native';
-import { Colors, Spacing, FontSize, BorderRadius } from '../theme';
+import { Spacing, FontSize, BorderRadius } from '../theme';
+import { ThemeColors } from '../theme/colors';
+import { useThemeColors, useFontScale } from '../contexts/AccessibilityContext';
interface HorizontalMenuProps {
items: string[];
@@ -9,11 +11,16 @@ interface HorizontalMenuProps {
}
export default function HorizontalMenu({ items, selected, onSelect }: HorizontalMenuProps) {
+ const colors = useThemeColors();
+ const fontScale = useFontScale();
+ const styles = useStyles(colors, fontScale);
+
return (
{items.map((item) => {
const isActive = item === selected;
@@ -22,6 +29,10 @@ export default function HorizontalMenu({ items, selected, onSelect }: Horizontal
key={item}
style={[styles.item, isActive && styles.itemActive]}
onPress={() => onSelect(item)}
+ accessibilityRole="tab"
+ accessibilityLabel={item}
+ accessibilityState={{ selected: isActive }}
+ accessibilityHint={`Filtrar por ${item}`}
>
{item}
@@ -33,30 +44,36 @@ export default function HorizontalMenu({ items, selected, onSelect }: Horizontal
);
}
-const styles = StyleSheet.create({
- container: {
- paddingHorizontal: Spacing.base,
- paddingVertical: Spacing.sm,
- gap: Spacing.sm,
- },
- item: {
- paddingHorizontal: Spacing.base,
- paddingVertical: Spacing.sm,
- borderRadius: BorderRadius.xl,
- backgroundColor: Colors.surface,
- borderWidth: 1,
- borderColor: Colors.border,
- },
- itemActive: {
- backgroundColor: Colors.primary,
- borderColor: Colors.primary,
- },
- text: {
- fontSize: FontSize.md,
- color: Colors.textSecondary,
- },
- textActive: {
- color: Colors.textOnPrimary,
- fontWeight: '600',
- },
-});
+function useStyles(colors: ThemeColors, fontScale: number) {
+ return useMemo(
+ () =>
+ StyleSheet.create({
+ container: {
+ paddingHorizontal: Spacing.base,
+ paddingVertical: Spacing.sm,
+ gap: Spacing.sm,
+ },
+ item: {
+ paddingHorizontal: Spacing.base,
+ paddingVertical: Spacing.sm,
+ borderRadius: BorderRadius.xl,
+ backgroundColor: colors.surface,
+ borderWidth: 1,
+ borderColor: colors.border,
+ },
+ itemActive: {
+ backgroundColor: colors.primary,
+ borderColor: colors.primary,
+ },
+ text: {
+ fontSize: FontSize.md * fontScale,
+ color: colors.textSecondary,
+ },
+ textActive: {
+ color: colors.textOnPrimary,
+ fontWeight: '600',
+ },
+ }),
+ [colors, fontScale],
+ );
+}
diff --git a/src/contexts/AccessibilityContext.tsx b/src/contexts/AccessibilityContext.tsx
new file mode 100644
index 0000000..c55f3bd
--- /dev/null
+++ b/src/contexts/AccessibilityContext.tsx
@@ -0,0 +1,157 @@
+import React, {
+ createContext,
+ useContext,
+ useState,
+ useEffect,
+ useCallback,
+ useMemo,
+ ReactNode,
+} from 'react';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+import { Colors, HighContrastColors, ThemeColors } from '../theme/colors';
+
+export type FontScale = 'normal' | 'grande' | 'maior';
+
+// Multiplicador aplicado à tipografia conforme a preferência do usuário.
+export const FONT_SCALE_FACTOR: Record = {
+ normal: 1,
+ grande: 1.2,
+ maior: 1.4,
+};
+
+const FONT_SCALE_LABEL: Record = {
+ normal: 'Normal',
+ grande: 'Grande',
+ maior: 'Maior',
+};
+
+const STORAGE_KEY_FONTE = '@App:acessibilidade:fontScale';
+const STORAGE_KEY_CONTRASTE = '@App:acessibilidade:highContrast';
+
+const ORDEM_FONTE: FontScale[] = ['normal', 'grande', 'maior'];
+
+interface AccessibilityContextData {
+ fontScale: FontScale;
+ fontScaleFactor: number;
+ fontScaleLabel: string;
+ highContrast: boolean;
+ isLoading: boolean;
+ colors: ThemeColors;
+ setFontScale(escala: FontScale): Promise;
+ cycleFontScale(): Promise;
+ setHighContrast(ativo: boolean): Promise;
+ toggleHighContrast(): Promise;
+}
+
+const AccessibilityContext = createContext(
+ {} as AccessibilityContextData,
+);
+
+interface AccessibilityProviderProps {
+ children: ReactNode;
+}
+
+export const AccessibilityProvider = ({ children }: AccessibilityProviderProps) => {
+ const [fontScale, setFontScaleState] = useState('normal');
+ const [highContrast, setHighContrastState] = useState(false);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ async function carregarPreferencias() {
+ try {
+ const [fonteSalva, contrasteSalvo] = await Promise.all([
+ AsyncStorage.getItem(STORAGE_KEY_FONTE),
+ AsyncStorage.getItem(STORAGE_KEY_CONTRASTE),
+ ]);
+ if (fonteSalva && fonteSalva in FONT_SCALE_FACTOR) {
+ setFontScaleState(fonteSalva as FontScale);
+ }
+ if (contrasteSalvo === 'true') {
+ setHighContrastState(true);
+ }
+ } catch (error) {
+ console.error(error);
+ } finally {
+ setIsLoading(false);
+ }
+ }
+ carregarPreferencias();
+ }, []);
+
+ const setFontScale = useCallback(async (escala: FontScale) => {
+ setFontScaleState(escala);
+ try {
+ await AsyncStorage.setItem(STORAGE_KEY_FONTE, escala);
+ } catch (error) {
+ console.error(error);
+ }
+ }, []);
+
+ const cycleFontScale = useCallback(async () => {
+ const indiceAtual = ORDEM_FONTE.indexOf(fontScale);
+ const proxima = ORDEM_FONTE[(indiceAtual + 1) % ORDEM_FONTE.length];
+ await setFontScale(proxima);
+ }, [fontScale, setFontScale]);
+
+ const setHighContrast = useCallback(async (ativo: boolean) => {
+ setHighContrastState(ativo);
+ try {
+ await AsyncStorage.setItem(STORAGE_KEY_CONTRASTE, ativo ? 'true' : 'false');
+ } catch (error) {
+ console.error(error);
+ }
+ }, []);
+
+ const toggleHighContrast = useCallback(async () => {
+ await setHighContrast(!highContrast);
+ }, [highContrast, setHighContrast]);
+
+ const value = useMemo(
+ () => ({
+ fontScale,
+ fontScaleFactor: FONT_SCALE_FACTOR[fontScale],
+ fontScaleLabel: FONT_SCALE_LABEL[fontScale],
+ highContrast,
+ isLoading,
+ colors: highContrast ? HighContrastColors : Colors,
+ setFontScale,
+ cycleFontScale,
+ setHighContrast,
+ toggleHighContrast,
+ }),
+ [
+ fontScale,
+ highContrast,
+ isLoading,
+ setFontScale,
+ cycleFontScale,
+ setHighContrast,
+ toggleHighContrast,
+ ],
+ );
+
+ return (
+
+ {children}
+
+ );
+};
+
+export function useAccessibility(): AccessibilityContextData {
+ return useContext(AccessibilityContext);
+}
+
+// Retorna a paleta ativa conforme a preferência de contraste.
+// Faz fallback para a paleta padrão fora do Provider.
+export function useThemeColors(): ThemeColors {
+ const ctx = useContext(AccessibilityContext);
+ return ctx?.colors ?? Colors;
+}
+
+// Retorna o multiplicador de fonte ativo, com fallback seguro (1).
+export function useFontScale(): number {
+ const ctx = useContext(AccessibilityContext);
+ return ctx?.fontScaleFactor ?? 1;
+}
+
+export default AccessibilityContext;
diff --git a/src/contexts/__tests__/AccessibilityContext.test.tsx b/src/contexts/__tests__/AccessibilityContext.test.tsx
new file mode 100644
index 0000000..4c21d21
--- /dev/null
+++ b/src/contexts/__tests__/AccessibilityContext.test.tsx
@@ -0,0 +1,100 @@
+import React from 'react';
+import { Text } from 'react-native';
+import { act, render, screen, waitFor } from '@testing-library/react-native';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+import {
+ AccessibilityProvider,
+ useAccessibility,
+} from '../AccessibilityContext';
+
+jest.mock('@react-native-async-storage/async-storage', () => ({
+ getItem: jest.fn(),
+ setItem: jest.fn(),
+}));
+
+const mockedAsyncStorage = AsyncStorage as jest.Mocked;
+
+let controle: ReturnType;
+
+function Sonda() {
+ controle = useAccessibility();
+ return (
+
+ {controle.fontScaleLabel}-{controle.highContrast ? 'alto' : 'normal'}
+
+ );
+}
+
+function renderizar() {
+ return render(
+
+
+ ,
+ );
+}
+
+describe('AccessibilityContext', () => {
+ beforeEach(() => {
+ mockedAsyncStorage.getItem.mockResolvedValue(null);
+ mockedAsyncStorage.setItem.mockResolvedValue();
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('usa os valores padrão quando não há nada persistido', async () => {
+ renderizar();
+
+ await waitFor(() => {
+ expect(screen.getByText('Normal-normal')).toBeTruthy();
+ });
+ expect(controle.fontScaleFactor).toBe(1);
+ });
+
+ it('persiste a escolha de alto contraste', async () => {
+ renderizar();
+ await waitFor(() => expect(controle).toBeDefined());
+
+ await act(async () => {
+ await controle.setHighContrast(true);
+ });
+
+ expect(mockedAsyncStorage.setItem).toHaveBeenCalledWith(
+ '@App:acessibilidade:highContrast',
+ 'true',
+ );
+ expect(controle.highContrast).toBe(true);
+ });
+
+ it('cicla o tamanho da fonte e persiste', async () => {
+ renderizar();
+ await waitFor(() => expect(controle).toBeDefined());
+
+ await act(async () => {
+ await controle.cycleFontScale();
+ });
+
+ expect(controle.fontScale).toBe('grande');
+ expect(controle.fontScaleFactor).toBeGreaterThan(1);
+ expect(mockedAsyncStorage.setItem).toHaveBeenCalledWith(
+ '@App:acessibilidade:fontScale',
+ 'grande',
+ );
+ });
+
+ it('carrega preferências previamente persistidas', async () => {
+ mockedAsyncStorage.getItem.mockImplementation(async (chave: string) => {
+ if (chave === '@App:acessibilidade:fontScale') return 'maior';
+ if (chave === '@App:acessibilidade:highContrast') return 'true';
+ return null;
+ });
+
+ renderizar();
+
+ await waitFor(() => {
+ expect(screen.getByText('Maior-alto')).toBeTruthy();
+ });
+ expect(controle.colors.background).toBeTruthy();
+ });
+});
diff --git a/src/screens/app/ConfiguracoesScreen.tsx b/src/screens/app/ConfiguracoesScreen.tsx
index c8df89e..896bd6d 100644
--- a/src/screens/app/ConfiguracoesScreen.tsx
+++ b/src/screens/app/ConfiguracoesScreen.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useMemo, useState } from 'react';
import {
View,
Text,
@@ -10,35 +10,90 @@ import {
Alert,
} from 'react-native';
import Ionicons from 'react-native-vector-icons/Ionicons';
+import { ThemeColors } from '../../theme/colors';
+import { useAccessibility, useThemeColors, useFontScale } from '../../contexts/AccessibilityContext';
-function SettingToggle({ label, value, onToggle }: { label: string; value: boolean; onToggle: (v: boolean) => void }) {
+function SettingToggle({
+ label,
+ value,
+ onToggle,
+ colors,
+ fontScale,
+ hint,
+}: {
+ label: string;
+ value: boolean;
+ onToggle: (v: boolean) => void;
+ colors: ThemeColors;
+ fontScale: number;
+ hint?: string;
+}) {
+ const styles = useStyles(colors, fontScale);
return (
{label}
);
}
-function SettingOption({ icon, label, value, onPress }: { icon: string; label: string; value?: string; onPress: () => void }) {
+function SettingOption({
+ icon,
+ label,
+ value,
+ onPress,
+ colors,
+ fontScale,
+ hint,
+}: {
+ icon: string;
+ label: string;
+ value?: string;
+ onPress: () => void;
+ colors: ThemeColors;
+ fontScale: number;
+ hint?: string;
+}) {
+ const styles = useStyles(colors, fontScale);
return (
-
-
+
+
{label}
{value && {value}}
-
+
);
}
export default function ConfiguracoesScreen() {
+ const colors = useThemeColors();
+ const fontScale = useFontScale();
+ const styles = useStyles(colors, fontScale);
+ const {
+ fontScaleLabel,
+ cycleFontScale,
+ highContrast,
+ setHighContrast,
+ } = useAccessibility();
+
const [lembretes, setLembretes] = useState(true);
const [amigos, setAmigos] = useState(false);
const [conquistas, setConquistas] = useState(true);
@@ -71,80 +126,114 @@ export default function ConfiguracoesScreen() {
- Notificações
+
+ Notificações
+
- Aparência
+
+ Aparência
+
- Vídeo e Armazenamento
+
+ Vídeo e Armazenamento
+
- Acessibilidade
+
+ Acessibilidade
+
{}}
+ value={fontScaleLabel}
+ onPress={cycleFontScale}
+ colors={colors}
+ fontScale={fontScale}
+ hint="Toque para alternar entre Normal, Grande e Maior"
/>
- {}}
+ value={highContrast}
+ onToggle={setHighContrast}
+ colors={colors}
+ fontScale={fontScale}
+ hint="Ativa cores de alto contraste para facilitar a leitura"
/>
- Sobre
+
+ Sobre
+
{}}
+ colors={colors}
+ fontScale={fontScale}
/>
{}}
+ colors={colors}
+ fontScale={fontScale}
/>
{}}
+ colors={colors}
+ fontScale={fontScale}
/>
@@ -152,58 +241,65 @@ export default function ConfiguracoesScreen() {
);
}
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#FFFFFF',
- },
- content: {
- paddingBottom: 40,
- },
- secao: {
- paddingHorizontal: 20,
- paddingTop: 20,
- },
- secaoTitulo: {
- fontSize: 13,
- fontWeight: '600',
- color: '#7A869A',
- textTransform: 'uppercase',
- letterSpacing: 0.5,
- marginBottom: 12,
- },
- toggleRow: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- paddingVertical: 12,
- borderBottomWidth: 1,
- borderBottomColor: '#F0F2F5',
- },
- toggleLabel: {
- fontSize: 16,
- color: '#1A1A1A',
- },
- optionRow: {
- flexDirection: 'row',
- alignItems: 'center',
- paddingVertical: 14,
- borderBottomWidth: 1,
- borderBottomColor: '#F0F2F5',
- },
- optionLabel: {
- flex: 1,
- fontSize: 16,
- color: '#1A1A1A',
- marginLeft: 12,
- },
- optionRight: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 4,
- },
- optionValue: {
- fontSize: 14,
- color: '#7A869A',
- },
-});
+function useStyles(colors: ThemeColors, fontScale: number) {
+ return useMemo(
+ () =>
+ StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: colors.background,
+ },
+ content: {
+ paddingBottom: 40,
+ },
+ secao: {
+ paddingHorizontal: 20,
+ paddingTop: 20,
+ },
+ secaoTitulo: {
+ fontSize: 13 * fontScale,
+ fontWeight: '600',
+ color: colors.textSecondary,
+ textTransform: 'uppercase',
+ letterSpacing: 0.5,
+ marginBottom: 12,
+ },
+ toggleRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingVertical: 12,
+ borderBottomWidth: 1,
+ borderBottomColor: colors.borderLight,
+ },
+ toggleLabel: {
+ flex: 1,
+ fontSize: 16 * fontScale,
+ color: colors.text,
+ },
+ optionRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingVertical: 14,
+ borderBottomWidth: 1,
+ borderBottomColor: colors.borderLight,
+ },
+ optionLabel: {
+ flex: 1,
+ fontSize: 16 * fontScale,
+ color: colors.text,
+ marginLeft: 12,
+ },
+ optionRight: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 4,
+ },
+ optionValue: {
+ fontSize: 14 * fontScale,
+ color: colors.textSecondary,
+ },
+ }),
+ [colors, fontScale],
+ );
+}
diff --git a/src/screens/app/HomeScreen.tsx b/src/screens/app/HomeScreen.tsx
index 82b46d4..d4c121a 100644
--- a/src/screens/app/HomeScreen.tsx
+++ b/src/screens/app/HomeScreen.tsx
@@ -1,8 +1,10 @@
-import React, { useCallback, useEffect, useState } from 'react';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, FlatList } from 'react-native';
import { useFocusEffect, useNavigation } from '@react-navigation/native';
import Ionicons from 'react-native-vector-icons/Ionicons';
-import { Colors, Spacing, FontSize, BorderRadius } from '../../theme';
+import { Spacing, FontSize, BorderRadius } from '../../theme';
+import { ThemeColors } from '../../theme/colors';
+import { useThemeColors, useFontScale } from '../../contexts/AccessibilityContext';
import { auth } from '../../services/firebase';
import { getCurrentUserById } from '../../services/models/user';
import { getCourseModulesOverview, ModuleOverview } from '../../services/models/courseOverview';
@@ -12,21 +14,34 @@ interface CourseCardProps {
progress: number;
locked: boolean;
onPress: () => void;
+ colors: ThemeColors;
+ fontScale: number;
}
-function CourseCard({ title, progress, locked, onPress }: CourseCardProps) {
+function CourseCard({ title, progress, locked, onPress, colors, fontScale }: CourseCardProps) {
const isCompleted = progress >= 100;
+ const styles = useStyles(colors, fontScale);
+
+ const estado = locked
+ ? 'Bloqueado'
+ : isCompleted
+ ? 'Concluído'
+ : `${progress}% concluído`;
return (
{title}
{locked ? (
-
+
) : (
{progress}%
)}
@@ -53,6 +68,9 @@ function CourseCard({ title, progress, locked, onPress }: CourseCardProps) {
}
export default function HomeScreen() {
+ const colors = useThemeColors();
+ const fontScale = useFontScale();
+ const styles = useStyles(colors, fontScale);
const [modules, setModules] = useState([]);
const [streak, setStreak] = useState(0);
const navigation = useNavigation();
@@ -95,12 +113,18 @@ export default function HomeScreen() {
return (
-
-
+
+
{streak} dias de ofensiva
- Módulos
+
+ Módulos
+
handleCardPress(item.id)}
+ colors={colors}
+ fontScale={fontScale}
/>
)}
showsVerticalScrollIndicator={false}
@@ -119,100 +145,106 @@ export default function HomeScreen() {
);
}
-const styles = StyleSheet.create({
- screenContainer: {
- flex: 1,
- backgroundColor: Colors.background,
- padding: Spacing.lg,
- },
- streakContainer: {
- flexDirection: 'row',
- alignItems: 'center',
- marginBottom: Spacing.base,
- gap: Spacing.sm,
- },
- streakText: {
- fontSize: FontSize.base,
- fontWeight: '600',
- color: Colors.streak,
- },
- title: {
- fontSize: FontSize.title,
- fontWeight: 'bold',
- color: Colors.accent,
- marginBottom: Spacing.lg,
- },
- cardContainer: {
- backgroundColor: Colors.card,
- borderRadius: BorderRadius.xl,
- borderWidth: 1.5,
- borderColor: '#6379F2',
- paddingVertical: Spacing.base,
- paddingHorizontal: Spacing.lg,
- marginBottom: Spacing.lg,
- shadowColor: '#000',
- shadowOffset: { width: 0, height: 2 },
- shadowOpacity: 0.05,
- shadowRadius: 4,
- elevation: 2,
- },
- cardLocked: {
- opacity: 0.5,
- },
- headerRow: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- marginBottom: Spacing.sm,
- },
- titleText: {
- fontSize: FontSize.base,
- color: Colors.textSecondary,
- fontWeight: '500',
- },
- percentageText: {
- fontSize: FontSize.base,
- color: Colors.textSecondary,
- fontWeight: '500',
- },
- progressBarTrack: {
- height: 8,
- backgroundColor: Colors.progressTrack,
- borderRadius: BorderRadius.sm,
- marginBottom: Spacing.base,
- overflow: 'hidden',
- },
- progressBarFill: {
- height: '100%',
- backgroundColor: Colors.progressFill,
- borderRadius: BorderRadius.sm,
- },
- footer: {
- alignItems: 'center',
- justifyContent: 'center',
- },
- continueText: {
- fontSize: FontSize.base,
- fontWeight: 'bold',
- color: Colors.primaryDark,
- },
- lockedText: {
- fontSize: FontSize.base,
- color: Colors.textLight,
- },
- completedContainer: {
- flexDirection: 'row',
- alignItems: 'center',
- },
- completedText: {
- fontSize: FontSize.base,
- fontWeight: 'bold',
- color: Colors.primaryDark,
- },
- checkIcon: {
- fontSize: FontSize.lg,
- fontWeight: 'bold',
- color: Colors.success,
- marginLeft: 6,
- },
-});
+function useStyles(colors: ThemeColors, fontScale: number) {
+ return useMemo(
+ () =>
+ StyleSheet.create({
+ screenContainer: {
+ flex: 1,
+ backgroundColor: colors.background,
+ padding: Spacing.lg,
+ },
+ streakContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: Spacing.base,
+ gap: Spacing.sm,
+ },
+ streakText: {
+ fontSize: FontSize.base * fontScale,
+ fontWeight: '600',
+ color: colors.streak,
+ },
+ title: {
+ fontSize: FontSize.title * fontScale,
+ fontWeight: 'bold',
+ color: colors.accent,
+ marginBottom: Spacing.lg,
+ },
+ cardContainer: {
+ backgroundColor: colors.card,
+ borderRadius: BorderRadius.xl,
+ borderWidth: 1.5,
+ borderColor: colors.borderAccent,
+ paddingVertical: Spacing.base,
+ paddingHorizontal: Spacing.lg,
+ marginBottom: Spacing.lg,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.05,
+ shadowRadius: 4,
+ elevation: 2,
+ },
+ cardLocked: {
+ opacity: 0.5,
+ },
+ headerRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: Spacing.sm,
+ },
+ titleText: {
+ fontSize: FontSize.base * fontScale,
+ color: colors.textSecondary,
+ fontWeight: '500',
+ },
+ percentageText: {
+ fontSize: FontSize.base * fontScale,
+ color: colors.textSecondary,
+ fontWeight: '500',
+ },
+ progressBarTrack: {
+ height: 8,
+ backgroundColor: colors.progressTrack,
+ borderRadius: BorderRadius.sm,
+ marginBottom: Spacing.base,
+ overflow: 'hidden',
+ },
+ progressBarFill: {
+ height: '100%',
+ backgroundColor: colors.progressFill,
+ borderRadius: BorderRadius.sm,
+ },
+ footer: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ continueText: {
+ fontSize: FontSize.base * fontScale,
+ fontWeight: 'bold',
+ color: colors.primaryDark,
+ },
+ lockedText: {
+ fontSize: FontSize.base * fontScale,
+ color: colors.textLight,
+ },
+ completedContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ completedText: {
+ fontSize: FontSize.base * fontScale,
+ fontWeight: 'bold',
+ color: colors.primaryDark,
+ },
+ checkIcon: {
+ fontSize: FontSize.lg * fontScale,
+ fontWeight: 'bold',
+ color: colors.success,
+ marginLeft: 6,
+ },
+ }),
+ [colors, fontScale],
+ );
+}
diff --git a/src/screens/app/NotificationScreen.tsx b/src/screens/app/NotificationScreen.tsx
index 9c1972f..2ab2883 100644
--- a/src/screens/app/NotificationScreen.tsx
+++ b/src/screens/app/NotificationScreen.tsx
@@ -9,6 +9,8 @@ import {
} from 'react-native';
import Ionicons from 'react-native-vector-icons/Ionicons';
import { Colors, Spacing, BorderRadius, FontSize } from '../../theme';
+import { ThemeColors } from '../../theme/colors';
+import { useThemeColors, useFontScale } from '../../contexts/AccessibilityContext';
type TipoNotificacao = 'lembrete' | 'conquista' | 'social' | 'sistema';
@@ -116,6 +118,8 @@ type CartaoProps = {
onAcao: (item: Notificacao) => void;
onMarcarLida: (id: string) => void;
onExcluir: (id: string) => void;
+ colors: ThemeColors;
+ fontScale: number;
};
function CartaoNotificacao({
@@ -124,8 +128,11 @@ function CartaoNotificacao({
onAcao,
onMarcarLida,
onExcluir,
+ colors,
+ fontScale,
}: CartaoProps) {
const config = CONFIG_TIPO[item.tipo];
+ const styles = useStyles(colors, fontScale);
return (
onLer(item.id)}
onLongPress={() => onExcluir(item.id)}
activeOpacity={0.7}
+ accessibilityRole="button"
+ accessibilityLabel={`${item.lida ? 'Lida' : 'Não lida'}. ${item.titulo}. ${item.mensagem}. ${item.tempo}`}
+ accessibilityHint="Toque para marcar como lida. Pressione e segure para excluir."
>
@@ -152,7 +162,9 @@ function CartaoNotificacao({
{item.mensagem}
- {!item.lida && }
+ {!item.lida && (
+
+ )}
@@ -160,6 +172,8 @@ function CartaoNotificacao({
style={[styles.botaoAcaoPrincipal, { backgroundColor: config.cor + '1A' }]}
onPress={() => onAcao(item)}
activeOpacity={0.7}
+ accessibilityRole="button"
+ accessibilityLabel={config.acaoLabel}
>
@@ -173,8 +187,10 @@ function CartaoNotificacao({
style={styles.botaoSecundario}
onPress={() => onMarcarLida(item.id)}
activeOpacity={0.7}
+ accessibilityRole="button"
+ accessibilityLabel="Marcar como lida"
>
-
+
Marcar como lida
)}
@@ -182,9 +198,11 @@ function CartaoNotificacao({
style={styles.botaoSecundario}
onPress={() => onExcluir(item.id)}
activeOpacity={0.7}
+ accessibilityRole="button"
+ accessibilityLabel="Excluir notificação"
>
-
-
+
+
Excluir
@@ -195,6 +213,9 @@ function CartaoNotificacao({
}
export default function NotificationScreen() {
+ const colors = useThemeColors();
+ const fontScale = useFontScale();
+ const styles = useStyles(colors, fontScale);
const [filtro, setFiltro] = useState<(typeof FILTROS)[number]>('Todas');
const [notificacoes, setNotificacoes] = useState(buscarNotificacoesMock);
@@ -242,7 +263,12 @@ export default function NotificationScreen() {
{naoLidas > 0 && (
-
+
Marcar todas como lidas
)}
@@ -260,6 +286,9 @@ export default function NotificationScreen() {
style={[styles.filtroChip, filtro === item && styles.filtroChipAtivo]}
onPress={() => setFiltro(item)}
activeOpacity={0.7}
+ accessibilityRole="tab"
+ accessibilityLabel={`Filtro ${item}`}
+ accessibilityState={{ selected: filtro === item }}
>
}
ListEmptyComponent={
-
+
Nenhuma notificação
}
@@ -290,6 +319,8 @@ export default function NotificationScreen() {
onAcao={executarAcao}
onMarcarLida={marcarComoLida}
onExcluir={excluir}
+ colors={colors}
+ fontScale={fontScale}
/>
)}
/>
@@ -297,157 +328,163 @@ export default function NotificationScreen() {
);
}
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: Colors.background,
- },
- headerActions: {
- flexDirection: 'row',
- justifyContent: 'flex-end',
- paddingHorizontal: Spacing.base,
- paddingTop: Spacing.sm,
- },
- marcarTodas: {
- fontSize: FontSize.sm,
- color: Colors.primary,
- fontWeight: '600',
- },
- filtrosWrapper: {
- borderBottomWidth: 1,
- borderBottomColor: Colors.borderLight,
- },
- filtros: {
- paddingHorizontal: Spacing.base,
- paddingVertical: Spacing.md,
- gap: Spacing.sm,
- },
- filtroChip: {
- paddingHorizontal: Spacing.base,
- paddingVertical: Spacing.sm,
- borderRadius: BorderRadius.round,
- backgroundColor: Colors.surface,
- marginRight: Spacing.sm,
- },
- filtroChipAtivo: {
- backgroundColor: Colors.primary,
- },
- filtroTexto: {
- fontSize: FontSize.md,
- color: Colors.textSecondary,
- fontWeight: '500',
- },
- filtroTextoAtivo: {
- color: Colors.textOnPrimary,
- },
- lista: {
- padding: Spacing.base,
- paddingBottom: Spacing.lg,
- },
- separador: {
- height: Spacing.md,
- },
- notificacao: {
- backgroundColor: Colors.card,
- borderRadius: BorderRadius.lg,
- borderWidth: 1,
- borderColor: Colors.borderLight,
- padding: Spacing.base,
- },
- notificacaoNaoLida: {
- backgroundColor: Colors.surface,
- borderColor: Colors.borderAccent,
- },
- linhaTopo: {
- flexDirection: 'row',
- alignItems: 'center',
- },
- iconeContainer: {
- width: 44,
- height: 44,
- borderRadius: BorderRadius.lg,
- justifyContent: 'center',
- alignItems: 'center',
- },
- notificacaoInfo: {
- flex: 1,
- marginLeft: Spacing.md,
- },
- notificacaoHeader: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- },
- notificacaoTitulo: {
- flex: 1,
- fontSize: FontSize.base,
- fontWeight: '500',
- color: Colors.text,
- },
- textoNaoLido: {
- fontWeight: '700',
- },
- tempo: {
- fontSize: FontSize.sm,
- color: Colors.textLight,
- marginLeft: Spacing.sm,
- },
- mensagem: {
- fontSize: FontSize.md,
- color: Colors.textSecondary,
- marginTop: Spacing.xs,
- lineHeight: 18,
- },
- indicadorNaoLido: {
- width: 8,
- height: 8,
- borderRadius: BorderRadius.round,
- backgroundColor: Colors.primary,
- marginLeft: Spacing.sm,
- },
- acoes: {
- marginTop: Spacing.md,
- paddingTop: Spacing.md,
- borderTopWidth: 1,
- borderTopColor: Colors.borderLight,
- gap: Spacing.sm,
- },
- botaoAcaoPrincipal: {
- flexDirection: 'row',
- alignItems: 'center',
- alignSelf: 'flex-start',
- gap: Spacing.xs,
- paddingHorizontal: Spacing.md,
- paddingVertical: Spacing.sm,
- borderRadius: BorderRadius.md,
- },
- botaoAcaoTexto: {
- fontSize: FontSize.md,
- fontWeight: '600',
- },
- acoesSecundarias: {
- flexDirection: 'row',
- flexWrap: 'wrap',
- gap: Spacing.base,
- },
- botaoSecundario: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: Spacing.xs,
- paddingVertical: Spacing.xs,
- },
- botaoSecundarioTexto: {
- fontSize: FontSize.sm,
- color: Colors.textSecondary,
- fontWeight: '500',
- },
- vazio: {
- alignItems: 'center',
- paddingTop: Spacing.xxxl + Spacing.lg,
- },
- vazioTexto: {
- fontSize: FontSize.base,
- color: Colors.textSecondary,
- marginTop: Spacing.md,
- },
-});
+function useStyles(colors: ThemeColors, fontScale: number) {
+ return useMemo(
+ () =>
+ StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: colors.background,
+ },
+ headerActions: {
+ flexDirection: 'row',
+ justifyContent: 'flex-end',
+ paddingHorizontal: Spacing.base,
+ paddingTop: Spacing.sm,
+ },
+ marcarTodas: {
+ fontSize: FontSize.sm * fontScale,
+ color: colors.primary,
+ fontWeight: '600',
+ },
+ filtrosWrapper: {
+ borderBottomWidth: 1,
+ borderBottomColor: colors.borderLight,
+ },
+ filtros: {
+ paddingHorizontal: Spacing.base,
+ paddingVertical: Spacing.md,
+ gap: Spacing.sm,
+ },
+ filtroChip: {
+ paddingHorizontal: Spacing.base,
+ paddingVertical: Spacing.sm,
+ borderRadius: BorderRadius.round,
+ backgroundColor: colors.surface,
+ marginRight: Spacing.sm,
+ },
+ filtroChipAtivo: {
+ backgroundColor: colors.primary,
+ },
+ filtroTexto: {
+ fontSize: FontSize.md * fontScale,
+ color: colors.textSecondary,
+ fontWeight: '500',
+ },
+ filtroTextoAtivo: {
+ color: colors.textOnPrimary,
+ },
+ lista: {
+ padding: Spacing.base,
+ paddingBottom: Spacing.lg,
+ },
+ separador: {
+ height: Spacing.md,
+ },
+ notificacao: {
+ backgroundColor: colors.card,
+ borderRadius: BorderRadius.lg,
+ borderWidth: 1,
+ borderColor: colors.borderLight,
+ padding: Spacing.base,
+ },
+ notificacaoNaoLida: {
+ backgroundColor: colors.surface,
+ borderColor: colors.borderAccent,
+ },
+ linhaTopo: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ iconeContainer: {
+ width: 44,
+ height: 44,
+ borderRadius: BorderRadius.lg,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ notificacaoInfo: {
+ flex: 1,
+ marginLeft: Spacing.md,
+ },
+ notificacaoHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ },
+ notificacaoTitulo: {
+ flex: 1,
+ fontSize: FontSize.base * fontScale,
+ fontWeight: '500',
+ color: colors.text,
+ },
+ textoNaoLido: {
+ fontWeight: '700',
+ },
+ tempo: {
+ fontSize: FontSize.sm * fontScale,
+ color: colors.textLight,
+ marginLeft: Spacing.sm,
+ },
+ mensagem: {
+ fontSize: FontSize.md * fontScale,
+ color: colors.textSecondary,
+ marginTop: Spacing.xs,
+ lineHeight: 18 * fontScale,
+ },
+ indicadorNaoLido: {
+ width: 8,
+ height: 8,
+ borderRadius: BorderRadius.round,
+ backgroundColor: colors.primary,
+ marginLeft: Spacing.sm,
+ },
+ acoes: {
+ marginTop: Spacing.md,
+ paddingTop: Spacing.md,
+ borderTopWidth: 1,
+ borderTopColor: colors.borderLight,
+ gap: Spacing.sm,
+ },
+ botaoAcaoPrincipal: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ alignSelf: 'flex-start',
+ gap: Spacing.xs,
+ paddingHorizontal: Spacing.md,
+ paddingVertical: Spacing.sm,
+ borderRadius: BorderRadius.md,
+ },
+ botaoAcaoTexto: {
+ fontSize: FontSize.md * fontScale,
+ fontWeight: '600',
+ },
+ acoesSecundarias: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ gap: Spacing.base,
+ },
+ botaoSecundario: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: Spacing.xs,
+ paddingVertical: Spacing.xs,
+ },
+ botaoSecundarioTexto: {
+ fontSize: FontSize.sm * fontScale,
+ color: colors.textSecondary,
+ fontWeight: '500',
+ },
+ vazio: {
+ alignItems: 'center',
+ paddingTop: Spacing.xxxl + Spacing.lg,
+ },
+ vazioTexto: {
+ fontSize: FontSize.base * fontScale,
+ color: colors.textSecondary,
+ marginTop: Spacing.md,
+ },
+ }),
+ [colors, fontScale],
+ );
+}
diff --git a/src/screens/app/PerfilScreen.tsx b/src/screens/app/PerfilScreen.tsx
index 903dc89..7142437 100644
--- a/src/screens/app/PerfilScreen.tsx
+++ b/src/screens/app/PerfilScreen.tsx
@@ -1,15 +1,28 @@
-import React, { useCallback, useState } from 'react';
+import React, { useCallback, useMemo, useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Image } from 'react-native';
import { useFocusEffect, useNavigation } from '@react-navigation/native';
import Ionicons from 'react-native-vector-icons/Ionicons';
import { auth } from '../../services/firebase';
import { getCurrentUserById, UserProfile } from '../../services/models/user';
import { signOutUser } from '../../services/auth';
-import { Colors, Spacing, FontSize, BorderRadius } from '../../theme';
+import { Spacing, FontSize, BorderRadius } from '../../theme';
+import { ThemeColors } from '../../theme/colors';
+import { useThemeColors, useFontScale } from '../../contexts/AccessibilityContext';
-function ProfileInfoRow({ label, value }: { label: string; value: string }) {
+function ProfileInfoRow({
+ label,
+ value,
+ colors,
+ fontScale,
+}: {
+ label: string;
+ value: string;
+ colors: ThemeColors;
+ fontScale: number;
+}) {
+ const styles = useStyles(colors, fontScale);
return (
-
+
{label}
{value}
@@ -17,6 +30,9 @@ function ProfileInfoRow({ label, value }: { label: string; value: string }) {
}
export default function PerfilScreen() {
+ const colors = useThemeColors();
+ const fontScale = useFontScale();
+ const styles = useStyles(colors, fontScale);
const navigation = useNavigation();
const [profile, setProfile] = useState(null);
@@ -44,63 +60,89 @@ export default function PerfilScreen() {
{profile?.profilePictureUrl ? (
-
+
) : (
-
+
)}
- {name}
+
+ {name}
+
{email}
-
+
{profile?.xp ?? 0}
XP Total
-
+
{profile?.streak?.current ?? 0}
Ofensiva
- Informações pessoais
-
-
-
+
+ Informações pessoais
+
+
+
+
navigation.navigate('EditProfile')}
+ accessibilityRole="button"
+ accessibilityLabel="Editar Perfil"
+ accessibilityHint="Abre a tela de edição do perfil"
>
-
+
Editar Perfil
-
+
navigation.navigate('ChangePassword')}
+ accessibilityRole="button"
+ accessibilityLabel="Alterar Senha"
+ accessibilityHint="Abre a tela de alteração de senha"
>
-
+
Alterar Senha
-
+
navigation.navigate('BugReport')}
+ accessibilityRole="button"
+ accessibilityLabel="Reportar Bug"
+ accessibilityHint="Abre a tela para reportar um problema"
>
-
+
Reportar Bug
-
+
-
-
+
+
Sair
@@ -108,123 +150,129 @@ export default function PerfilScreen() {
);
}
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: Colors.background,
- },
- content: {
- padding: Spacing.lg,
- },
- avatarSection: {
- alignItems: 'center',
- marginBottom: Spacing.xl,
- },
- avatar: {
- width: 96,
- height: 96,
- borderRadius: 48,
- backgroundColor: Colors.primary,
- justifyContent: 'center',
- alignItems: 'center',
- marginBottom: Spacing.md,
- overflow: 'hidden',
- },
- avatarImage: {
- width: '100%',
- height: '100%',
- },
- name: {
- fontSize: FontSize.xl,
- fontWeight: 'bold',
- color: Colors.text,
- },
- email: {
- fontSize: FontSize.md,
- color: Colors.textSecondary,
- marginTop: Spacing.xs,
- },
- statsRow: {
- flexDirection: 'row',
- justifyContent: 'space-around',
- backgroundColor: Colors.surface,
- borderRadius: BorderRadius.lg,
- padding: Spacing.base,
- marginBottom: Spacing.xl,
- },
- statBox: {
- alignItems: 'center',
- },
- statValue: {
- fontSize: FontSize.xxl,
- fontWeight: 'bold',
- color: Colors.primary,
- },
- statLabel: {
- fontSize: FontSize.sm,
- color: Colors.textSecondary,
- marginTop: Spacing.xs,
- },
- menuSection: {
- gap: Spacing.sm,
- },
- infoCard: {
- backgroundColor: Colors.card,
- borderRadius: BorderRadius.lg,
- borderWidth: 1,
- borderColor: Colors.border,
- padding: Spacing.base,
- marginBottom: Spacing.xl,
- gap: Spacing.sm,
- },
- infoTitle: {
- fontSize: FontSize.base,
- fontWeight: '600',
- color: Colors.text,
- marginBottom: Spacing.xs,
- },
- infoRow: {
- gap: Spacing.xs,
- },
- infoLabel: {
- fontSize: FontSize.sm,
- color: Colors.textSecondary,
- },
- infoValue: {
- fontSize: FontSize.base,
- color: Colors.text,
- fontWeight: '500',
- },
- menuItem: {
- flexDirection: 'row',
- alignItems: 'center',
- backgroundColor: Colors.card,
- borderRadius: BorderRadius.lg,
- borderWidth: 1,
- borderColor: Colors.border,
- padding: Spacing.base,
- gap: Spacing.md,
- },
- menuText: {
- flex: 1,
- fontSize: FontSize.base,
- color: Colors.text,
- },
- logoutButton: {
- flexDirection: 'row',
- alignItems: 'center',
- backgroundColor: Colors.card,
- borderRadius: BorderRadius.lg,
- borderWidth: 1,
- borderColor: Colors.error,
- padding: Spacing.base,
- gap: Spacing.md,
- marginTop: Spacing.md,
- },
- logoutText: {
- flex: 1,
- fontSize: FontSize.base,
- color: Colors.error,
- fontWeight: '600',
- },
-});
+function useStyles(colors: ThemeColors, fontScale: number) {
+ return useMemo(
+ () =>
+ StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: colors.background,
+ },
+ content: {
+ padding: Spacing.lg,
+ },
+ avatarSection: {
+ alignItems: 'center',
+ marginBottom: Spacing.xl,
+ },
+ avatar: {
+ width: 96,
+ height: 96,
+ borderRadius: 48,
+ backgroundColor: colors.primary,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginBottom: Spacing.md,
+ overflow: 'hidden',
+ },
+ avatarImage: {
+ width: '100%',
+ height: '100%',
+ },
+ name: {
+ fontSize: FontSize.xl * fontScale,
+ fontWeight: 'bold',
+ color: colors.text,
+ },
+ email: {
+ fontSize: FontSize.md * fontScale,
+ color: colors.textSecondary,
+ marginTop: Spacing.xs,
+ },
+ statsRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-around',
+ backgroundColor: colors.surface,
+ borderRadius: BorderRadius.lg,
+ padding: Spacing.base,
+ marginBottom: Spacing.xl,
+ },
+ statBox: {
+ alignItems: 'center',
+ },
+ statValue: {
+ fontSize: FontSize.xxl * fontScale,
+ fontWeight: 'bold',
+ color: colors.primary,
+ },
+ statLabel: {
+ fontSize: FontSize.sm * fontScale,
+ color: colors.textSecondary,
+ marginTop: Spacing.xs,
+ },
+ menuSection: {
+ gap: Spacing.sm,
+ },
+ infoCard: {
+ backgroundColor: colors.card,
+ borderRadius: BorderRadius.lg,
+ borderWidth: 1,
+ borderColor: colors.border,
+ padding: Spacing.base,
+ marginBottom: Spacing.xl,
+ gap: Spacing.sm,
+ },
+ infoTitle: {
+ fontSize: FontSize.base * fontScale,
+ fontWeight: '600',
+ color: colors.text,
+ marginBottom: Spacing.xs,
+ },
+ infoRow: {
+ gap: Spacing.xs,
+ },
+ infoLabel: {
+ fontSize: FontSize.sm * fontScale,
+ color: colors.textSecondary,
+ },
+ infoValue: {
+ fontSize: FontSize.base * fontScale,
+ color: colors.text,
+ fontWeight: '500',
+ },
+ menuItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ backgroundColor: colors.card,
+ borderRadius: BorderRadius.lg,
+ borderWidth: 1,
+ borderColor: colors.border,
+ padding: Spacing.base,
+ gap: Spacing.md,
+ },
+ menuText: {
+ flex: 1,
+ fontSize: FontSize.base * fontScale,
+ color: colors.text,
+ },
+ logoutButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ backgroundColor: colors.card,
+ borderRadius: BorderRadius.lg,
+ borderWidth: 1,
+ borderColor: colors.error,
+ padding: Spacing.base,
+ gap: Spacing.md,
+ marginTop: Spacing.md,
+ },
+ logoutText: {
+ flex: 1,
+ fontSize: FontSize.base * fontScale,
+ color: colors.error,
+ fontWeight: '600',
+ },
+ }),
+ [colors, fontScale],
+ );
+}
diff --git a/src/screens/auth/LoginScreen.tsx b/src/screens/auth/LoginScreen.tsx
index b48aaee..a2c1bf5 100644
--- a/src/screens/auth/LoginScreen.tsx
+++ b/src/screens/auth/LoginScreen.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useMemo, useState } from 'react';
import {
View,
Text,
@@ -14,7 +14,9 @@ import {
import { AuthScreenProps } from '../../@types/navigation';
import { signInWithEmail } from '../../services/models/user';
import { isUserAuthenticated } from '../../services/auth';
-import { Colors, Spacing, FontSize, BorderRadius } from '../../theme';
+import { Spacing, FontSize, BorderRadius } from '../../theme';
+import { ThemeColors } from '../../theme/colors';
+import { useThemeColors, useFontScale } from '../../contexts/AccessibilityContext';
import Ionicons from 'react-native-vector-icons/Ionicons';
function getLoginErrorMessage(error: unknown) {
@@ -36,6 +38,9 @@ function getLoginErrorMessage(error: unknown) {
}
export default function LoginScreen({ navigation }: AuthScreenProps<'Login'>) {
+ const colors = useThemeColors();
+ const fontScale = useFontScale();
+ const styles = useStyles(colors, fontScale);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
@@ -67,15 +72,25 @@ export default function LoginScreen({ navigation }: AuthScreenProps<'Login'>) {
>
- Login
+
+ Login
+
{/* Social buttons */}
-
+
Facebook
-
+
Google
@@ -92,11 +107,13 @@ export default function LoginScreen({ navigation }: AuthScreenProps<'Login'>) {
{/* Password */}
@@ -104,16 +121,23 @@ export default function LoginScreen({ navigation }: AuthScreenProps<'Login'>) {
- setShowPassword(!showPassword)} style={styles.eyeIcon}>
+ setShowPassword(!showPassword)}
+ style={styles.eyeIcon}
+ accessibilityRole="button"
+ accessibilityLabel={showPassword ? 'Ocultar senha' : 'Mostrar senha'}
+ >
@@ -122,6 +146,8 @@ export default function LoginScreen({ navigation }: AuthScreenProps<'Login'>) {
navigation.navigate('ForgotPassword')}
style={styles.forgotRow}
+ accessibilityRole="button"
+ accessibilityLabel="Esqueceu a senha?"
>
Esqueceu a senha?
@@ -131,16 +157,23 @@ export default function LoginScreen({ navigation }: AuthScreenProps<'Login'>) {
style={[styles.button, loading && styles.buttonDisabled]}
onPress={handleLogin}
disabled={loading}
+ accessibilityRole="button"
+ accessibilityLabel="Entrar"
+ accessibilityState={{ disabled: loading, busy: loading }}
>
{loading ? (
-
+
) : (
Log In
)}
{/* Register link */}
- navigation.navigate('Register')}>
+ navigation.navigate('Register')}
+ accessibilityRole="button"
+ accessibilityLabel="Não tem uma conta? Cadastre-se"
+ >
Não tem uma conta?{' '}
Cadastre-se
@@ -152,112 +185,118 @@ export default function LoginScreen({ navigation }: AuthScreenProps<'Login'>) {
);
}
-const styles = StyleSheet.create({
- flex: { flex: 1, backgroundColor: Colors.background },
- scroll: { flexGrow: 1 },
- container: {
- flex: 1,
- paddingHorizontal: Spacing.xl,
- paddingTop: Spacing.xxxl + Spacing.xl,
- paddingBottom: Spacing.xxl,
- },
- title: {
- fontSize: FontSize.title,
- fontWeight: 'bold',
- color: Colors.accent,
- marginBottom: 80,
- },
- socialRow: {
- flexDirection: 'row',
- gap: Spacing.md,
- marginBottom: Spacing.xl,
- },
- socialButton: {
- flex: 1,
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- gap: Spacing.sm,
- backgroundColor: Colors.surface,
- paddingVertical: Spacing.md,
- borderRadius: BorderRadius.lg,
- },
- socialText: {
- fontSize: FontSize.base,
- color: Colors.text,
- fontWeight: '500',
- },
- divider: {
- flexDirection: 'row',
- alignItems: 'center',
- marginBottom: Spacing.xl,
- },
- dividerLine: {
- flex: 1,
- height: 1,
- backgroundColor: Colors.border,
- },
- dividerText: {
- marginHorizontal: Spacing.md,
- color: Colors.textSecondary,
- fontSize: FontSize.base,
- },
- input: {
- backgroundColor: Colors.surface,
- borderRadius: BorderRadius.lg,
- paddingHorizontal: Spacing.base,
- paddingVertical: Spacing.md,
- fontSize: FontSize.base,
- color: Colors.text,
- marginBottom: Spacing.md,
- height: 56,
- },
- passwordContainer: {
- flexDirection: 'row',
- alignItems: 'center',
- backgroundColor: Colors.surface,
- borderRadius: BorderRadius.lg,
- height: 56,
- paddingHorizontal: Spacing.base,
- },
- passwordInput: {
- flex: 1,
- fontSize: FontSize.base,
- color: Colors.text,
- },
- eyeIcon: {
- padding: Spacing.xs,
- },
- forgotRow: {
- alignSelf: 'flex-end',
- marginTop: Spacing.sm,
- marginBottom: Spacing.xl,
- },
- forgotText: {
- fontSize: FontSize.sm,
- color: Colors.textSecondary,
- },
- button: {
- backgroundColor: Colors.accent,
- borderRadius: 14,
- height: 60,
- alignItems: 'center',
- justifyContent: 'center',
- marginBottom: Spacing.xl,
- },
- buttonDisabled: { opacity: 0.7 },
- buttonText: {
- color: Colors.textOnPrimary,
- fontSize: FontSize.base,
- fontWeight: '700',
- },
- registerText: {
- textAlign: 'center',
- fontSize: FontSize.base,
- color: Colors.text,
- },
- registerLink: {
- color: Colors.accent,
- fontWeight: '600',
- },
-});
+function useStyles(colors: ThemeColors, fontScale: number) {
+ return useMemo(
+ () =>
+ StyleSheet.create({
+ flex: { flex: 1, backgroundColor: colors.background },
+ scroll: { flexGrow: 1 },
+ container: {
+ flex: 1,
+ paddingHorizontal: Spacing.xl,
+ paddingTop: Spacing.xxxl + Spacing.xl,
+ paddingBottom: Spacing.xxl,
+ },
+ title: {
+ fontSize: FontSize.title * fontScale,
+ fontWeight: 'bold',
+ color: colors.accent,
+ marginBottom: 80,
+ },
+ socialRow: {
+ flexDirection: 'row',
+ gap: Spacing.md,
+ marginBottom: Spacing.xl,
+ },
+ socialButton: {
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: Spacing.sm,
+ backgroundColor: colors.surface,
+ paddingVertical: Spacing.md,
+ borderRadius: BorderRadius.lg,
+ },
+ socialText: {
+ fontSize: FontSize.base * fontScale,
+ color: colors.text,
+ fontWeight: '500',
+ },
+ divider: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: Spacing.xl,
+ },
+ dividerLine: {
+ flex: 1,
+ height: 1,
+ backgroundColor: colors.border,
+ },
+ dividerText: {
+ marginHorizontal: Spacing.md,
+ color: colors.textSecondary,
+ fontSize: FontSize.base * fontScale,
+ },
+ input: {
+ backgroundColor: colors.surface,
+ borderRadius: BorderRadius.lg,
+ paddingHorizontal: Spacing.base,
+ paddingVertical: Spacing.md,
+ fontSize: FontSize.base * fontScale,
+ color: colors.text,
+ marginBottom: Spacing.md,
+ height: 56,
+ },
+ passwordContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ backgroundColor: colors.surface,
+ borderRadius: BorderRadius.lg,
+ height: 56,
+ paddingHorizontal: Spacing.base,
+ },
+ passwordInput: {
+ flex: 1,
+ fontSize: FontSize.base * fontScale,
+ color: colors.text,
+ },
+ eyeIcon: {
+ padding: Spacing.xs,
+ },
+ forgotRow: {
+ alignSelf: 'flex-end',
+ marginTop: Spacing.sm,
+ marginBottom: Spacing.xl,
+ },
+ forgotText: {
+ fontSize: FontSize.sm * fontScale,
+ color: colors.textSecondary,
+ },
+ button: {
+ backgroundColor: colors.accent,
+ borderRadius: 14,
+ height: 60,
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginBottom: Spacing.xl,
+ },
+ buttonDisabled: { opacity: 0.7 },
+ buttonText: {
+ color: colors.textOnPrimary,
+ fontSize: FontSize.base * fontScale,
+ fontWeight: '700',
+ },
+ registerText: {
+ textAlign: 'center',
+ fontSize: FontSize.base * fontScale,
+ color: colors.text,
+ },
+ registerLink: {
+ color: colors.accent,
+ fontWeight: '600',
+ },
+ }),
+ [colors, fontScale],
+ );
+}
diff --git a/src/screens/auth/RegisterScreen.tsx b/src/screens/auth/RegisterScreen.tsx
index 9c1a285..8315cee 100644
--- a/src/screens/auth/RegisterScreen.tsx
+++ b/src/screens/auth/RegisterScreen.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useMemo, useState } from 'react';
import {
View,
Text,
@@ -15,6 +15,8 @@ import { AuthScreenProps } from '../../@types/navigation';
import { registerUserWithEmail } from '../../services/models/user';
import { validatePasswordStrength, PasswordStrength } from '../../services/auth';
import { Colors, Spacing, FontSize, BorderRadius } from '../../theme';
+import { ThemeColors } from '../../theme/colors';
+import { useThemeColors, useFontScale } from '../../contexts/AccessibilityContext';
import Ionicons from 'react-native-vector-icons/Ionicons';
const FORCA_CONFIG: Record = {
@@ -24,6 +26,9 @@ const FORCA_CONFIG: Record) {
+ const colors = useThemeColors();
+ const fontScale = useFontScale();
+ const styles = useStyles(colors, fontScale);
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
@@ -81,15 +86,25 @@ export default function RegisterScreen({ navigation }: AuthScreenProps<'Register
>
- Cadastro
+
+ Cadastro
+
{/* Social buttons */}
-
+
Facebook
-
+
Google
@@ -106,34 +121,45 @@ export default function RegisterScreen({ navigation }: AuthScreenProps<'Register
- setShowPassword(!showPassword)} style={styles.eyeIcon}>
+ setShowPassword(!showPassword)}
+ style={styles.eyeIcon}
+ accessibilityRole="button"
+ accessibilityLabel={showPassword ? 'Ocultar senha' : 'Mostrar senha'}
+ >
@@ -169,9 +195,12 @@ export default function RegisterScreen({ navigation }: AuthScreenProps<'Register
style={styles.termsRow}
onPress={() => setAcceptedTerms(!acceptedTerms)}
activeOpacity={0.7}
+ accessibilityRole="checkbox"
+ accessibilityLabel="Eu concordo com os Termos de Serviço e Política de Privacidade"
+ accessibilityState={{ checked: acceptedTerms }}
>
- {acceptedTerms && }
+ {acceptedTerms && }
Eu concordo com os{' '}
@@ -186,16 +215,23 @@ export default function RegisterScreen({ navigation }: AuthScreenProps<'Register
style={[styles.button, loading && styles.buttonDisabled]}
onPress={handleRegister}
disabled={loading}
+ accessibilityRole="button"
+ accessibilityLabel="Criar Conta"
+ accessibilityState={{ disabled: loading, busy: loading }}
>
{loading ? (
-
+
) : (
Criar Conta
)}
{/* Login link */}
- navigation.navigate('Login')}>
+ navigation.navigate('Login')}
+ accessibilityRole="button"
+ accessibilityLabel="Já tem uma conta? Entrar"
+ >
Já tem uma conta?{' '}
Entrar
@@ -207,162 +243,168 @@ export default function RegisterScreen({ navigation }: AuthScreenProps<'Register
);
}
-const styles = StyleSheet.create({
- flex: { flex: 1, backgroundColor: Colors.background },
- scroll: { flexGrow: 1 },
- container: {
- flex: 1,
- paddingHorizontal: Spacing.xl,
- paddingTop: Spacing.xxxl + Spacing.xl,
- paddingBottom: Spacing.xxl,
- },
- title: {
- fontSize: FontSize.title,
- fontWeight: 'bold',
- color: Colors.accent,
- marginBottom: Spacing.xxxl,
- },
- socialRow: {
- flexDirection: 'row',
- gap: Spacing.md,
- marginBottom: Spacing.xl,
- },
- socialButton: {
- flex: 1,
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- gap: Spacing.sm,
- backgroundColor: Colors.surface,
- paddingVertical: Spacing.md,
- borderRadius: BorderRadius.lg,
- },
- socialText: {
- fontSize: FontSize.base,
- color: Colors.text,
- fontWeight: '500',
- },
- divider: {
- flexDirection: 'row',
- alignItems: 'center',
- marginBottom: Spacing.xl,
- },
- dividerLine: {
- flex: 1,
- height: 1,
- backgroundColor: Colors.border,
- },
- dividerText: {
- marginHorizontal: Spacing.md,
- color: Colors.textSecondary,
- fontSize: FontSize.base,
- },
- input: {
- backgroundColor: Colors.surface,
- borderRadius: BorderRadius.lg,
- paddingHorizontal: Spacing.base,
- paddingVertical: Spacing.md,
- fontSize: FontSize.base,
- color: Colors.text,
- marginBottom: Spacing.md,
- height: 56,
- },
- passwordContainer: {
- flexDirection: 'row',
- alignItems: 'center',
- backgroundColor: Colors.surface,
- borderRadius: BorderRadius.lg,
- marginBottom: Spacing.base,
- height: 56,
- paddingHorizontal: Spacing.base,
- },
- passwordInput: {
- flex: 1,
- fontSize: FontSize.base,
- color: Colors.text,
- },
- eyeIcon: {
- padding: Spacing.xs,
- },
- strengthContainer: {
- marginBottom: Spacing.base,
- },
- strengthBarTrack: {
- height: 6,
- borderRadius: BorderRadius.round,
- backgroundColor: Colors.progressTrack,
- overflow: 'hidden',
- },
- strengthBarFill: {
- height: '100%',
- borderRadius: BorderRadius.round,
- },
- strengthLabel: {
- marginTop: Spacing.xs,
- fontSize: FontSize.sm,
- fontWeight: '600',
- },
- strengthHints: {
- marginTop: Spacing.xs,
- gap: 2,
- },
- strengthHint: {
- fontSize: FontSize.sm,
- color: Colors.textSecondary,
- lineHeight: 18,
- },
- termsRow: {
- flexDirection: 'row',
- alignItems: 'flex-start',
- gap: Spacing.md,
- marginBottom: Spacing.xl,
- },
- checkbox: {
- width: 22,
- height: 22,
- borderRadius: BorderRadius.sm,
- borderWidth: 1.5,
- borderColor: Colors.border,
- alignItems: 'center',
- justifyContent: 'center',
- marginTop: 2,
- flexShrink: 0,
- },
- checkboxChecked: {
- backgroundColor: Colors.accent,
- borderColor: Colors.accent,
- },
- termsText: {
- flex: 1,
- fontSize: FontSize.sm,
- color: Colors.text,
- lineHeight: 20,
- },
- termsLink: {
- color: Colors.accent,
- fontWeight: '500',
- },
- button: {
- backgroundColor: Colors.accent,
- borderRadius: 14,
- height: 60,
- alignItems: 'center',
- justifyContent: 'center',
- marginBottom: Spacing.xl,
- },
- buttonDisabled: { opacity: 0.7 },
- buttonText: {
- color: Colors.textOnPrimary,
- fontSize: FontSize.base,
- fontWeight: '700',
- },
- loginText: {
- textAlign: 'left',
- fontSize: FontSize.base,
- color: Colors.text,
- },
- loginLink: {
- color: Colors.accent,
- fontWeight: '600',
- },
-});
+function useStyles(colors: ThemeColors, fontScale: number) {
+ return useMemo(
+ () =>
+ StyleSheet.create({
+ flex: { flex: 1, backgroundColor: colors.background },
+ scroll: { flexGrow: 1 },
+ container: {
+ flex: 1,
+ paddingHorizontal: Spacing.xl,
+ paddingTop: Spacing.xxxl + Spacing.xl,
+ paddingBottom: Spacing.xxl,
+ },
+ title: {
+ fontSize: FontSize.title * fontScale,
+ fontWeight: 'bold',
+ color: colors.accent,
+ marginBottom: Spacing.xxxl,
+ },
+ socialRow: {
+ flexDirection: 'row',
+ gap: Spacing.md,
+ marginBottom: Spacing.xl,
+ },
+ socialButton: {
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: Spacing.sm,
+ backgroundColor: colors.surface,
+ paddingVertical: Spacing.md,
+ borderRadius: BorderRadius.lg,
+ },
+ socialText: {
+ fontSize: FontSize.base * fontScale,
+ color: colors.text,
+ fontWeight: '500',
+ },
+ divider: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: Spacing.xl,
+ },
+ dividerLine: {
+ flex: 1,
+ height: 1,
+ backgroundColor: colors.border,
+ },
+ dividerText: {
+ marginHorizontal: Spacing.md,
+ color: colors.textSecondary,
+ fontSize: FontSize.base * fontScale,
+ },
+ input: {
+ backgroundColor: colors.surface,
+ borderRadius: BorderRadius.lg,
+ paddingHorizontal: Spacing.base,
+ paddingVertical: Spacing.md,
+ fontSize: FontSize.base * fontScale,
+ color: colors.text,
+ marginBottom: Spacing.md,
+ height: 56,
+ },
+ passwordContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ backgroundColor: colors.surface,
+ borderRadius: BorderRadius.lg,
+ marginBottom: Spacing.base,
+ height: 56,
+ paddingHorizontal: Spacing.base,
+ },
+ passwordInput: {
+ flex: 1,
+ fontSize: FontSize.base * fontScale,
+ color: colors.text,
+ },
+ eyeIcon: {
+ padding: Spacing.xs,
+ },
+ strengthContainer: {
+ marginBottom: Spacing.base,
+ },
+ strengthBarTrack: {
+ height: 6,
+ borderRadius: BorderRadius.round,
+ backgroundColor: colors.progressTrack,
+ overflow: 'hidden',
+ },
+ strengthBarFill: {
+ height: '100%',
+ borderRadius: BorderRadius.round,
+ },
+ strengthLabel: {
+ marginTop: Spacing.xs,
+ fontSize: FontSize.sm * fontScale,
+ fontWeight: '600',
+ },
+ strengthHints: {
+ marginTop: Spacing.xs,
+ gap: 2,
+ },
+ strengthHint: {
+ fontSize: FontSize.sm * fontScale,
+ color: colors.textSecondary,
+ lineHeight: 18 * fontScale,
+ },
+ termsRow: {
+ flexDirection: 'row',
+ alignItems: 'flex-start',
+ gap: Spacing.md,
+ marginBottom: Spacing.xl,
+ },
+ checkbox: {
+ width: 22,
+ height: 22,
+ borderRadius: BorderRadius.sm,
+ borderWidth: 1.5,
+ borderColor: colors.border,
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginTop: 2,
+ flexShrink: 0,
+ },
+ checkboxChecked: {
+ backgroundColor: colors.accent,
+ borderColor: colors.accent,
+ },
+ termsText: {
+ flex: 1,
+ fontSize: FontSize.sm * fontScale,
+ color: colors.text,
+ lineHeight: 20 * fontScale,
+ },
+ termsLink: {
+ color: colors.accent,
+ fontWeight: '500',
+ },
+ button: {
+ backgroundColor: colors.accent,
+ borderRadius: 14,
+ height: 60,
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginBottom: Spacing.xl,
+ },
+ buttonDisabled: { opacity: 0.7 },
+ buttonText: {
+ color: colors.textOnPrimary,
+ fontSize: FontSize.base * fontScale,
+ fontWeight: '700',
+ },
+ loginText: {
+ textAlign: 'left',
+ fontSize: FontSize.base * fontScale,
+ color: colors.text,
+ },
+ loginLink: {
+ color: colors.accent,
+ fontWeight: '600',
+ },
+ }),
+ [colors, fontScale],
+ );
+}
diff --git a/src/theme/colors.ts b/src/theme/colors.ts
index 9b9d6d2..016a56b 100644
--- a/src/theme/colors.ts
+++ b/src/theme/colors.ts
@@ -29,3 +29,40 @@ export const Colors = {
progressTrack: '#F0F2F5',
progressFill: '#2D4CC8',
} as const;
+
+export type ThemeColors = { [K in keyof typeof Colors]: string };
+
+// Paleta de alto contraste (WCAG 2.2 AA).
+// Fundo claro com textos quase pretos e elementos saturados escuros,
+// garantindo razão de contraste alta para baixa visão.
+export const HighContrastColors: ThemeColors = {
+ primary: '#3A00B8',
+ primaryDark: '#1A237E',
+ accent: '#0B3CC1',
+ error: '#B00020',
+ success: '#0B6E2E',
+ warning: '#8A4B00',
+
+ background: '#FFFFFF',
+ surface: '#F2F2F2',
+ card: '#FFFFFF',
+
+ text: '#000000',
+ textSecondary: '#1F1F1F',
+ textLight: '#3D3D3D',
+ textOnPrimary: '#FFFFFF',
+
+ border: '#000000',
+ borderLight: '#5C5C5C',
+ borderAccent: '#0B3CC1',
+
+ tabActive: '#3A00B8',
+ tabInactive: '#3D3D3D',
+
+ podium: '#3A00B8',
+ gold: '#7A5C00',
+ streak: '#A33A00',
+
+ progressTrack: '#D6D6D6',
+ progressFill: '#1A237E',
+};